init rust. WIP: tls for inbounds

This commit is contained in:
Ultradesu
2025-09-18 02:56:59 +03:00
parent 777af49ebf
commit 8aff8f2fb5
206 changed files with 14301 additions and 21560 deletions

View File

@@ -0,0 +1,243 @@
use sea_orm::entity::prelude::*;
use sea_orm::{Set, ActiveModelTrait};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "certificates")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub name: String,
#[sea_orm(column_name = "cert_type")]
pub cert_type: String,
pub domain: String,
#[serde(skip_serializing)]
pub cert_data: Vec<u8>,
#[serde(skip_serializing)]
pub key_data: Vec<u8>,
#[serde(skip_serializing)]
pub chain_data: Option<Vec<u8>>,
pub expires_at: DateTimeUtc,
pub auto_renew: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::server::Entity")]
Servers,
#[sea_orm(has_many = "super::server_inbound::Entity")]
ServerInbounds,
}
impl Related<super::server::Entity> for Entity {
fn to() -> RelationDef {
Relation::Servers.def()
}
}
impl Related<super::server_inbound::Entity> for Entity {
fn to() -> RelationDef {
Relation::ServerInbounds.def()
}
}
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
}
}
fn before_save<'life0, 'async_trait, C>(
mut self,
_db: &'life0 C,
insert: bool,
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
where
'life0: 'async_trait,
C: 'async_trait + ConnectionTrait,
Self: 'async_trait,
{
Box::pin(async move {
if !insert {
self.updated_at = Set(chrono::Utc::now());
}
Ok(self)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CertificateType {
SelfSigned,
Imported,
}
impl From<CertificateType> for String {
fn from(cert_type: CertificateType) -> Self {
match cert_type {
CertificateType::SelfSigned => "self_signed".to_string(),
CertificateType::Imported => "imported".to_string(),
}
}
}
impl From<String> for CertificateType {
fn from(s: String) -> Self {
match s.as_str() {
"self_signed" => CertificateType::SelfSigned,
"imported" => CertificateType::Imported,
_ => CertificateType::SelfSigned,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateCertificateDto {
pub name: String,
pub cert_type: String,
pub domain: String,
pub auto_renew: bool,
#[serde(default)]
pub certificate_pem: String,
#[serde(default)]
pub private_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateCertificateDto {
pub name: Option<String>,
pub auto_renew: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertificateResponse {
pub id: Uuid,
pub name: String,
pub cert_type: String,
pub domain: String,
pub expires_at: DateTimeUtc,
pub auto_renew: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub has_cert_data: bool,
pub has_key_data: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertificateDetailsResponse {
pub id: Uuid,
pub name: String,
pub cert_type: String,
pub domain: String,
pub expires_at: DateTimeUtc,
pub auto_renew: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub certificate_pem: String,
pub has_private_key: bool,
}
impl From<Model> for CertificateResponse {
fn from(cert: Model) -> Self {
Self {
id: cert.id,
name: cert.name,
cert_type: cert.cert_type,
domain: cert.domain,
expires_at: cert.expires_at,
auto_renew: cert.auto_renew,
created_at: cert.created_at,
updated_at: cert.updated_at,
has_cert_data: !cert.cert_data.is_empty(),
has_key_data: !cert.key_data.is_empty(),
}
}
}
impl From<Model> for CertificateDetailsResponse {
fn from(cert: Model) -> Self {
let certificate_pem = cert.certificate_pem();
let has_private_key = !cert.key_data.is_empty();
Self {
id: cert.id,
name: cert.name,
cert_type: cert.cert_type,
domain: cert.domain,
expires_at: cert.expires_at,
auto_renew: cert.auto_renew,
created_at: cert.created_at,
updated_at: cert.updated_at,
certificate_pem,
has_private_key,
}
}
}
impl Model {
#[allow(dead_code)]
pub fn is_expired(&self) -> bool {
self.expires_at < chrono::Utc::now()
}
#[allow(dead_code)]
pub fn expires_soon(&self, days: i64) -> bool {
let threshold = chrono::Utc::now() + chrono::Duration::days(days);
self.expires_at < threshold
}
/// Get certificate data as PEM string
pub fn certificate_pem(&self) -> String {
String::from_utf8_lossy(&self.cert_data).to_string()
}
/// Get private key data as PEM string
pub fn private_key_pem(&self) -> String {
String::from_utf8_lossy(&self.key_data).to_string()
}
pub fn apply_update(self, dto: UpdateCertificateDto) -> ActiveModel {
let mut active_model: ActiveModel = self.into();
if let Some(name) = dto.name {
active_model.name = Set(name);
}
if let Some(auto_renew) = dto.auto_renew {
active_model.auto_renew = Set(auto_renew);
}
active_model
}
}
impl From<CreateCertificateDto> for ActiveModel {
fn from(dto: CreateCertificateDto) -> Self {
Self {
name: Set(dto.name),
cert_type: Set(dto.cert_type),
domain: Set(dto.domain),
cert_data: Set(dto.certificate_pem.into_bytes()),
key_data: Set(dto.private_key.into_bytes()),
chain_data: Set(None),
expires_at: Set(chrono::Utc::now() + chrono::Duration::days(90)), // Default 90 days
auto_renew: Set(dto.auto_renew),
..Self::new()
}
}
}

View File

@@ -0,0 +1,278 @@
use sea_orm::entity::prelude::*;
use sea_orm::{Set, ActiveModelTrait};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "inbound_templates")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub protocol: String,
pub default_port: i32,
pub base_settings: Value,
pub stream_settings: Value,
pub requires_tls: bool,
pub requires_domain: bool,
pub variables: Value,
pub is_active: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::server_inbound::Entity")]
ServerInbounds,
}
impl Related<super::server_inbound::Entity> for Entity {
fn to() -> RelationDef {
Relation::ServerInbounds.def()
}
}
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
}
}
fn before_save<'life0, 'async_trait, C>(
mut self,
_db: &'life0 C,
insert: bool,
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
where
'life0: 'async_trait,
C: 'async_trait + ConnectionTrait,
Self: 'async_trait,
{
Box::pin(async move {
if !insert {
self.updated_at = Set(chrono::Utc::now());
}
Ok(self)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Protocol {
Vless,
Vmess,
Trojan,
Shadowsocks,
}
impl From<Protocol> for String {
fn from(protocol: Protocol) -> Self {
match protocol {
Protocol::Vless => "vless".to_string(),
Protocol::Vmess => "vmess".to_string(),
Protocol::Trojan => "trojan".to_string(),
Protocol::Shadowsocks => "shadowsocks".to_string(),
}
}
}
impl From<String> for Protocol {
fn from(s: String) -> Self {
match s.as_str() {
"vless" => Protocol::Vless,
"vmess" => Protocol::Vmess,
"trojan" => Protocol::Trojan,
"shadowsocks" => Protocol::Shadowsocks,
_ => Protocol::Vless,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateVariable {
pub key: String,
pub var_type: VariableType,
pub required: bool,
pub default_value: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VariableType {
String,
Number,
Path,
Domain,
Port,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateInboundTemplateDto {
pub name: String,
pub protocol: String,
pub default_port: i32,
pub requires_tls: bool,
pub config_template: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateInboundTemplateDto {
pub name: Option<String>,
pub description: Option<String>,
pub default_port: Option<i32>,
pub base_settings: Option<Value>,
pub stream_settings: Option<Value>,
pub requires_tls: Option<bool>,
pub requires_domain: Option<bool>,
pub variables: Option<Vec<TemplateVariable>>,
pub is_active: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InboundTemplateResponse {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub protocol: String,
pub default_port: i32,
pub base_settings: Value,
pub stream_settings: Value,
pub requires_tls: bool,
pub requires_domain: bool,
pub variables: Vec<TemplateVariable>,
pub is_active: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
impl From<Model> for InboundTemplateResponse {
fn from(template: Model) -> Self {
let variables = template.get_variables();
Self {
id: template.id,
name: template.name,
description: template.description,
protocol: template.protocol,
default_port: template.default_port,
base_settings: template.base_settings,
stream_settings: template.stream_settings,
requires_tls: template.requires_tls,
requires_domain: template.requires_domain,
variables,
is_active: template.is_active,
created_at: template.created_at,
updated_at: template.updated_at,
}
}
}
impl From<CreateInboundTemplateDto> for ActiveModel {
fn from(dto: CreateInboundTemplateDto) -> Self {
// Parse config_template as JSON or use default
let config_json: Value = serde_json::from_str(&dto.config_template)
.unwrap_or_else(|_| serde_json::json!({}));
Self {
name: Set(dto.name),
description: Set(None),
protocol: Set(dto.protocol),
default_port: Set(dto.default_port),
base_settings: Set(config_json.clone()),
stream_settings: Set(serde_json::json!({})),
requires_tls: Set(dto.requires_tls),
requires_domain: Set(false),
variables: Set(Value::Array(vec![])),
is_active: Set(true),
..Self::new()
}
}
}
impl Model {
pub fn get_variables(&self) -> Vec<TemplateVariable> {
serde_json::from_value(self.variables.clone()).unwrap_or_default()
}
#[allow(dead_code)]
pub fn apply_variables(&self, values: &serde_json::Map<String, Value>) -> Result<(Value, Value), String> {
let base_settings = self.base_settings.clone();
let stream_settings = self.stream_settings.clone();
// Replace variables in JSON using simple string replacement
let base_str = base_settings.to_string();
let stream_str = stream_settings.to_string();
let mut result_base = base_str;
let mut result_stream = stream_str;
for (key, value) in values {
let placeholder = format!("${{{}}}", key);
let replacement = match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
_ => value.to_string(),
};
result_base = result_base.replace(&placeholder, &replacement);
result_stream = result_stream.replace(&placeholder, &replacement);
}
let final_base: Value = serde_json::from_str(&result_base)
.map_err(|e| format!("Invalid base settings after variable substitution: {}", e))?;
let final_stream: Value = serde_json::from_str(&result_stream)
.map_err(|e| format!("Invalid stream settings after variable substitution: {}", e))?;
Ok((final_base, final_stream))
}
pub fn apply_update(self, dto: UpdateInboundTemplateDto) -> ActiveModel {
let mut active_model: ActiveModel = self.into();
if let Some(name) = dto.name {
active_model.name = Set(name);
}
if let Some(description) = dto.description {
active_model.description = Set(Some(description));
}
if let Some(default_port) = dto.default_port {
active_model.default_port = Set(default_port);
}
if let Some(base_settings) = dto.base_settings {
active_model.base_settings = Set(base_settings);
}
if let Some(stream_settings) = dto.stream_settings {
active_model.stream_settings = Set(stream_settings);
}
if let Some(requires_tls) = dto.requires_tls {
active_model.requires_tls = Set(requires_tls);
}
if let Some(requires_domain) = dto.requires_domain {
active_model.requires_domain = Set(requires_domain);
}
if let Some(variables) = dto.variables {
active_model.variables = Set(serde_json::to_value(variables).unwrap_or(Value::Array(vec![])));
}
if let Some(is_active) = dto.is_active {
active_model.is_active = Set(is_active);
}
active_model
}
}

View File

@@ -0,0 +1,168 @@
use sea_orm::entity::prelude::*;
use sea_orm::{Set, ActiveModelTrait};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "inbound_users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub server_inbound_id: Uuid,
pub username: String,
pub email: String,
pub xray_user_id: String,
pub level: i32,
pub is_active: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::server_inbound::Entity",
from = "Column::ServerInboundId",
to = "super::server_inbound::Column::Id"
)]
ServerInbound,
}
impl Related<super::server_inbound::Entity> for Entity {
fn to() -> RelationDef {
Relation::ServerInbound.def()
}
}
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
}
}
fn before_save<'life0, 'async_trait, C>(
mut self,
_db: &'life0 C,
insert: bool,
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
where
'life0: 'async_trait,
C: 'async_trait + ConnectionTrait,
Self: 'async_trait,
{
Box::pin(async move {
if !insert {
self.updated_at = Set(chrono::Utc::now());
}
Ok(self)
})
}
}
/// Inbound user creation data transfer object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateInboundUserDto {
pub server_inbound_id: Uuid,
pub username: String,
pub level: Option<i32>,
}
impl CreateInboundUserDto {
/// Generate email in format: username@OutFleet
pub fn generate_email(&self) -> String {
format!("{}@OutFleet", self.username)
}
/// Generate UUID for xray user
pub fn generate_xray_user_id(&self) -> String {
Uuid::new_v4().to_string()
}
}
/// Inbound user update data transfer object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateInboundUserDto {
pub username: Option<String>,
pub level: Option<i32>,
pub is_active: Option<bool>,
}
impl From<CreateInboundUserDto> for ActiveModel {
fn from(dto: CreateInboundUserDto) -> Self {
let email = dto.generate_email();
let xray_user_id = dto.generate_xray_user_id();
Self {
server_inbound_id: Set(dto.server_inbound_id),
username: Set(dto.username),
email: Set(email),
xray_user_id: Set(xray_user_id),
level: Set(dto.level.unwrap_or(0)),
is_active: Set(true),
..Self::new()
}
}
}
impl Model {
/// Update this model with data from UpdateInboundUserDto
pub fn apply_update(self, dto: UpdateInboundUserDto) -> ActiveModel {
let mut active_model: ActiveModel = self.into();
if let Some(username) = dto.username {
let new_email = format!("{}@OutFleet", username);
active_model.username = Set(username);
active_model.email = Set(new_email);
}
if let Some(level) = dto.level {
active_model.level = Set(level);
}
if let Some(is_active) = dto.is_active {
active_model.is_active = Set(is_active);
}
active_model
}
}
/// Response model for inbound user
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InboundUserResponse {
pub id: Uuid,
pub server_inbound_id: Uuid,
pub username: String,
pub email: String,
pub xray_user_id: String,
pub level: i32,
pub is_active: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<Model> for InboundUserResponse {
fn from(model: Model) -> Self {
Self {
id: model.id,
server_inbound_id: model.server_inbound_id,
username: model.username,
email: model.email,
xray_user_id: model.xray_user_id,
level: model.level,
is_active: model.is_active,
created_at: model.created_at.to_rfc3339(),
updated_at: model.updated_at.to_rfc3339(),
}
}
}

View File

@@ -0,0 +1,16 @@
pub mod user;
pub mod certificate;
pub mod inbound_template;
pub mod server;
pub mod server_inbound;
pub mod user_access;
pub mod inbound_users;
pub mod prelude {
pub use super::certificate::Entity as Certificate;
pub use super::inbound_template::Entity as InboundTemplate;
pub use super::server::Entity as Server;
pub use super::server_inbound::Entity as ServerInbound;
pub use super::user_access::Entity as UserAccess;
pub use super::inbound_users::Entity as InboundUsers;
}

View File

@@ -0,0 +1,212 @@
use sea_orm::entity::prelude::*;
use sea_orm::{Set, ActiveModelTrait};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "servers")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub name: String,
pub hostname: String,
pub grpc_port: i32,
#[serde(skip_serializing)]
pub api_credentials: Option<String>,
pub status: String,
pub default_certificate_id: Option<Uuid>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::certificate::Entity",
from = "Column::DefaultCertificateId",
to = "super::certificate::Column::Id"
)]
DefaultCertificate,
#[sea_orm(has_many = "super::server_inbound::Entity")]
ServerInbounds,
}
impl Related<super::certificate::Entity> for Entity {
fn to() -> RelationDef {
Relation::DefaultCertificate.def()
}
}
impl Related<super::server_inbound::Entity> for Entity {
fn to() -> RelationDef {
Relation::ServerInbounds.def()
}
}
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
status: Set(ServerStatus::Unknown.into()),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
}
}
fn before_save<'life0, 'async_trait, C>(
mut self,
_db: &'life0 C,
insert: bool,
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
where
'life0: 'async_trait,
C: 'async_trait + ConnectionTrait,
Self: 'async_trait,
{
Box::pin(async move {
if !insert {
self.updated_at = Set(chrono::Utc::now());
}
Ok(self)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerStatus {
Unknown,
Online,
Offline,
Error,
Connecting,
}
impl From<ServerStatus> for String {
fn from(status: ServerStatus) -> Self {
match status {
ServerStatus::Unknown => "unknown".to_string(),
ServerStatus::Online => "online".to_string(),
ServerStatus::Offline => "offline".to_string(),
ServerStatus::Error => "error".to_string(),
ServerStatus::Connecting => "connecting".to_string(),
}
}
}
impl From<String> for ServerStatus {
fn from(s: String) -> Self {
match s.as_str() {
"online" => ServerStatus::Online,
"offline" => ServerStatus::Offline,
"error" => ServerStatus::Error,
"connecting" => ServerStatus::Connecting,
_ => ServerStatus::Unknown,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateServerDto {
pub name: String,
pub hostname: String,
pub grpc_port: Option<i32>,
pub api_credentials: Option<String>,
pub default_certificate_id: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateServerDto {
pub name: Option<String>,
pub hostname: Option<String>,
pub grpc_port: Option<i32>,
pub api_credentials: Option<String>,
pub status: Option<String>,
pub default_certificate_id: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerResponse {
pub id: Uuid,
pub name: String,
pub hostname: String,
pub grpc_port: i32,
pub status: String,
pub default_certificate_id: Option<Uuid>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub has_credentials: bool,
}
impl From<CreateServerDto> for ActiveModel {
fn from(dto: CreateServerDto) -> Self {
Self {
name: Set(dto.name),
hostname: Set(dto.hostname),
grpc_port: Set(dto.grpc_port.unwrap_or(2053)),
api_credentials: Set(dto.api_credentials),
status: Set("unknown".to_string()),
default_certificate_id: Set(dto.default_certificate_id),
..Self::new()
}
}
}
impl From<Model> for ServerResponse {
fn from(server: Model) -> Self {
Self {
id: server.id,
name: server.name,
hostname: server.hostname,
grpc_port: server.grpc_port,
status: server.status,
default_certificate_id: server.default_certificate_id,
created_at: server.created_at,
updated_at: server.updated_at,
has_credentials: server.api_credentials.is_some(),
}
}
}
impl Model {
pub fn apply_update(self, dto: UpdateServerDto) -> ActiveModel {
let mut active_model: ActiveModel = self.into();
if let Some(name) = dto.name {
active_model.name = Set(name);
}
if let Some(hostname) = dto.hostname {
active_model.hostname = Set(hostname);
}
if let Some(grpc_port) = dto.grpc_port {
active_model.grpc_port = Set(grpc_port);
}
if let Some(api_credentials) = dto.api_credentials {
active_model.api_credentials = Set(Some(api_credentials));
}
if let Some(status) = dto.status {
active_model.status = Set(status);
}
if let Some(default_certificate_id) = dto.default_certificate_id {
active_model.default_certificate_id = Set(Some(default_certificate_id));
}
active_model
}
pub fn get_grpc_endpoint(&self) -> String {
format!("{}:{}", self.hostname, self.grpc_port)
}
#[allow(dead_code)]
pub fn get_status(&self) -> ServerStatus {
self.status.clone().into()
}
}

View File

@@ -0,0 +1,204 @@
use sea_orm::entity::prelude::*;
use sea_orm::{Set, ActiveModelTrait};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "server_inbounds")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub server_id: Uuid,
pub template_id: Uuid,
pub tag: String,
pub port_override: Option<i32>,
pub certificate_id: Option<Uuid>,
pub variable_values: Value,
pub is_active: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::server::Entity",
from = "Column::ServerId",
to = "super::server::Column::Id"
)]
Server,
#[sea_orm(
belongs_to = "super::inbound_template::Entity",
from = "Column::TemplateId",
to = "super::inbound_template::Column::Id"
)]
Template,
#[sea_orm(
belongs_to = "super::certificate::Entity",
from = "Column::CertificateId",
to = "super::certificate::Column::Id"
)]
Certificate,
}
impl Related<super::server::Entity> for Entity {
fn to() -> RelationDef {
Relation::Server.def()
}
}
impl Related<super::inbound_template::Entity> for Entity {
fn to() -> RelationDef {
Relation::Template.def()
}
}
impl Related<super::certificate::Entity> for Entity {
fn to() -> RelationDef {
Relation::Certificate.def()
}
}
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
}
}
fn before_save<'life0, 'async_trait, C>(
mut self,
_db: &'life0 C,
insert: bool,
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
where
'life0: 'async_trait,
C: 'async_trait + ConnectionTrait,
Self: 'async_trait,
{
Box::pin(async move {
if !insert {
self.updated_at = Set(chrono::Utc::now());
}
Ok(self)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateServerInboundDto {
pub template_id: Uuid,
pub port: i32,
pub certificate_id: Option<Uuid>,
pub is_active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateServerInboundDto {
pub tag: Option<String>,
pub port_override: Option<i32>,
pub certificate_id: Option<Uuid>,
pub variable_values: Option<serde_json::Map<String, Value>>,
pub is_active: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInboundResponse {
pub id: Uuid,
pub server_id: Uuid,
pub template_id: Uuid,
pub tag: String,
pub port: i32,
pub certificate_id: Option<Uuid>,
pub variable_values: Value,
pub is_active: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
// Populated by joins (simplified for now)
pub template_name: Option<String>,
pub certificate_name: Option<String>,
}
impl From<Model> for ServerInboundResponse {
fn from(inbound: Model) -> Self {
Self {
id: inbound.id,
server_id: inbound.server_id,
template_id: inbound.template_id,
tag: inbound.tag,
port: inbound.port_override.unwrap_or(443), // Default port if not set
certificate_id: inbound.certificate_id,
variable_values: inbound.variable_values,
is_active: inbound.is_active,
created_at: inbound.created_at,
updated_at: inbound.updated_at,
template_name: None, // Will be filled by repository if needed
certificate_name: None, // Will be filled by repository if needed
}
}
}
impl Model {
pub fn apply_update(self, dto: UpdateServerInboundDto) -> ActiveModel {
let mut active_model: ActiveModel = self.into();
if let Some(tag) = dto.tag {
active_model.tag = Set(tag);
}
if let Some(port_override) = dto.port_override {
active_model.port_override = Set(Some(port_override));
}
if let Some(certificate_id) = dto.certificate_id {
active_model.certificate_id = Set(Some(certificate_id));
}
if let Some(variable_values) = dto.variable_values {
active_model.variable_values = Set(Value::Object(variable_values));
}
if let Some(is_active) = dto.is_active {
active_model.is_active = Set(is_active);
}
active_model
}
#[allow(dead_code)]
pub fn get_variable_values(&self) -> serde_json::Map<String, Value> {
if let Value::Object(map) = &self.variable_values {
map.clone()
} else {
serde_json::Map::new()
}
}
#[allow(dead_code)]
pub fn get_effective_port(&self, template_default_port: i32) -> i32 {
self.port_override.unwrap_or(template_default_port)
}
}
impl From<CreateServerInboundDto> for ActiveModel {
fn from(dto: CreateServerInboundDto) -> Self {
Self {
template_id: Set(dto.template_id),
tag: Set(format!("inbound-{}", Uuid::new_v4())), // Generate unique tag
port_override: Set(Some(dto.port)),
certificate_id: Set(dto.certificate_id),
variable_values: Set(Value::Object(serde_json::Map::new())),
is_active: Set(dto.is_active),
..Self::new()
}
}
}

View File

@@ -0,0 +1,185 @@
use sea_orm::entity::prelude::*;
use sea_orm::{Set, ActiveModelTrait};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
/// User display name
pub name: String,
/// Optional comment/description about the user
#[sea_orm(column_type = "Text")]
pub comment: Option<String>,
/// Optional Telegram user ID for bot integration
pub telegram_id: Option<i64>,
/// When the user was registered/created
pub created_at: DateTimeUtc,
/// Last time user record was updated
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {
/// Called before insert and update
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
}
}
/// Called before update
fn before_save<'life0, 'async_trait, C>(
mut self,
_db: &'life0 C,
insert: bool,
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
where
'life0: 'async_trait,
C: 'async_trait + ConnectionTrait,
Self: 'async_trait,
{
Box::pin(async move {
if !insert {
self.updated_at = Set(chrono::Utc::now());
}
Ok(self)
})
}
}
/// User creation data transfer object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserDto {
pub name: String,
pub comment: Option<String>,
pub telegram_id: Option<i64>,
}
/// User update data transfer object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUserDto {
pub name: Option<String>,
pub comment: Option<String>,
pub telegram_id: Option<i64>,
}
impl From<CreateUserDto> for ActiveModel {
fn from(dto: CreateUserDto) -> Self {
Self {
name: Set(dto.name),
comment: Set(dto.comment),
telegram_id: Set(dto.telegram_id),
..Self::new()
}
}
}
impl Model {
/// Update this model with data from UpdateUserDto
pub fn apply_update(self, dto: UpdateUserDto) -> ActiveModel {
let mut active_model: ActiveModel = self.into();
if let Some(name) = dto.name {
active_model.name = Set(name);
}
if let Some(comment) = dto.comment {
active_model.comment = Set(Some(comment));
} else if dto.comment.is_some() {
// Explicitly set to None if Some(None) was passed
active_model.comment = Set(None);
}
if dto.telegram_id.is_some() {
active_model.telegram_id = Set(dto.telegram_id);
}
active_model
}
/// Check if user has Telegram integration
#[allow(dead_code)]
pub fn has_telegram(&self) -> bool {
self.telegram_id.is_some()
}
/// Get display name with optional comment
#[allow(dead_code)]
pub fn display_name(&self) -> String {
match &self.comment {
Some(comment) if !comment.is_empty() => format!("{} ({})", self.name, comment),
_ => self.name.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_user_dto_conversion() {
let dto = CreateUserDto {
name: "Test User".to_string(),
comment: Some("Test comment".to_string()),
telegram_id: Some(123456789),
};
let active_model: ActiveModel = dto.into();
assert_eq!(active_model.name.unwrap(), "Test User");
assert_eq!(active_model.comment.unwrap(), Some("Test comment".to_string()));
assert_eq!(active_model.telegram_id.unwrap(), Some(123456789));
}
#[test]
fn test_user_display_name() {
let user = Model {
id: Uuid::new_v4(),
name: "John Doe".to_string(),
comment: Some("Admin user".to_string()),
telegram_id: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
assert_eq!(user.display_name(), "John Doe (Admin user)");
let user_no_comment = Model {
comment: None,
..user
};
assert_eq!(user_no_comment.display_name(), "John Doe");
}
#[test]
fn test_has_telegram() {
let user_with_telegram = Model {
id: Uuid::new_v4(),
name: "User".to_string(),
comment: None,
telegram_id: Some(123456789),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let user_without_telegram = Model {
telegram_id: None,
..user_with_telegram.clone()
};
assert!(user_with_telegram.has_telegram());
assert!(!user_without_telegram.has_telegram());
}
}

View File

@@ -0,0 +1,188 @@
use sea_orm::entity::prelude::*;
use sea_orm::{Set, ActiveModelTrait};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "user_access")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
/// User ID this access is for
pub user_id: Uuid,
/// Server ID this access applies to
pub server_id: Uuid,
/// Server inbound ID this access applies to
pub server_inbound_id: Uuid,
/// User's unique identifier in xray (UUID for VLESS/VMess, password for Trojan)
pub xray_user_id: String,
/// User's email in xray
pub xray_email: String,
/// User level in xray (0-255)
pub level: i32,
/// Whether this access is currently active
pub is_active: bool,
/// When this access was created
pub created_at: DateTimeUtc,
/// Last time this access was updated
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
#[sea_orm(
belongs_to = "super::server::Entity",
from = "Column::ServerId",
to = "super::server::Column::Id"
)]
Server,
#[sea_orm(
belongs_to = "super::server_inbound::Entity",
from = "Column::ServerInboundId",
to = "super::server_inbound::Column::Id"
)]
ServerInbound,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl Related<super::server::Entity> for Entity {
fn to() -> RelationDef {
Relation::Server.def()
}
}
impl Related<super::server_inbound::Entity> for Entity {
fn to() -> RelationDef {
Relation::ServerInbound.def()
}
}
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
}
}
fn before_save<'life0, 'async_trait, C>(
mut self,
_db: &'life0 C,
insert: bool,
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
where
'life0: 'async_trait,
C: 'async_trait + ConnectionTrait,
Self: 'async_trait,
{
Box::pin(async move {
if !insert {
self.updated_at = Set(chrono::Utc::now());
}
Ok(self)
})
}
}
/// User access creation data transfer object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserAccessDto {
pub user_id: Uuid,
pub server_id: Uuid,
pub server_inbound_id: Uuid,
pub xray_user_id: String,
pub xray_email: String,
pub level: Option<i32>,
}
/// User access update data transfer object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUserAccessDto {
pub is_active: Option<bool>,
pub level: Option<i32>,
}
impl From<CreateUserAccessDto> for ActiveModel {
fn from(dto: CreateUserAccessDto) -> Self {
Self {
user_id: Set(dto.user_id),
server_id: Set(dto.server_id),
server_inbound_id: Set(dto.server_inbound_id),
xray_user_id: Set(dto.xray_user_id),
xray_email: Set(dto.xray_email),
level: Set(dto.level.unwrap_or(0)),
is_active: Set(true),
..Self::new()
}
}
}
impl Model {
/// Update this model with data from UpdateUserAccessDto
pub fn apply_update(self, dto: UpdateUserAccessDto) -> ActiveModel {
let mut active_model: ActiveModel = self.into();
if let Some(is_active) = dto.is_active {
active_model.is_active = Set(is_active);
}
if let Some(level) = dto.level {
active_model.level = Set(level);
}
active_model
}
}
/// Response model for user access
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAccessResponse {
pub id: Uuid,
pub user_id: Uuid,
pub server_id: Uuid,
pub server_inbound_id: Uuid,
pub xray_user_id: String,
pub xray_email: String,
pub level: i32,
pub is_active: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<Model> for UserAccessResponse {
fn from(model: Model) -> Self {
Self {
id: model.id,
user_id: model.user_id,
server_id: model.server_id,
server_inbound_id: model.server_inbound_id,
xray_user_id: model.xray_user_id,
xray_email: model.xray_email,
level: model.level,
is_active: model.is_active,
created_at: model.created_at.to_rfc3339(),
updated_at: model.updated_at.to_rfc3339(),
}
}
}