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(),
}
}
}

View File

@@ -0,0 +1,135 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Create users table
manager
.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(
ColumnDef::new(Users::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Users::Name)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(Users::Comment)
.text()
.null(),
)
.col(
ColumnDef::new(Users::TelegramId)
.big_integer()
.null(),
)
.col(
ColumnDef::new(Users::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Users::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
// Create index on name for faster searches
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_users_name")
.table(Users::Table)
.col(Users::Name)
.to_owned(),
)
.await?;
// Create unique index on telegram_id (if not null)
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_users_telegram_id")
.table(Users::Table)
.col(Users::TelegramId)
.unique()
.to_owned(),
)
.await?;
// Create index on created_at for sorting
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_users_created_at")
.table(Users::Table)
.col(Users::CreatedAt)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Drop indexes first
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_users_created_at")
.to_owned(),
)
.await?;
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_users_telegram_id")
.to_owned(),
)
.await?;
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_users_name")
.to_owned(),
)
.await?;
// Drop table
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Name,
Comment,
TelegramId,
CreatedAt,
UpdatedAt,
}

View File

@@ -0,0 +1,120 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Certificates::Table)
.if_not_exists()
.col(
ColumnDef::new(Certificates::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Certificates::Name)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(Certificates::CertType)
.string_len(50)
.not_null(),
)
.col(
ColumnDef::new(Certificates::Domain)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(Certificates::CertData)
.blob()
.not_null(),
)
.col(
ColumnDef::new(Certificates::KeyData)
.blob()
.not_null(),
)
.col(
ColumnDef::new(Certificates::ChainData)
.blob()
.null(),
)
.col(
ColumnDef::new(Certificates::ExpiresAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Certificates::AutoRenew)
.boolean()
.default(false)
.not_null(),
)
.col(
ColumnDef::new(Certificates::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Certificates::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
// Index on domain for faster lookups
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_certificates_domain")
.table(Certificates::Table)
.col(Certificates::Domain)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_certificates_domain")
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Certificates::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Certificates {
Table,
Id,
Name,
CertType,
Domain,
CertData,
KeyData,
ChainData,
ExpiresAt,
AutoRenew,
CreatedAt,
UpdatedAt,
}

View File

@@ -0,0 +1,155 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(InboundTemplates::Table)
.if_not_exists()
.col(
ColumnDef::new(InboundTemplates::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(InboundTemplates::Name)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::Description)
.text()
.null(),
)
.col(
ColumnDef::new(InboundTemplates::Protocol)
.string_len(50)
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::DefaultPort)
.integer()
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::BaseSettings)
.json()
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::StreamSettings)
.json()
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::RequiresTls)
.boolean()
.default(false)
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::RequiresDomain)
.boolean()
.default(false)
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::Variables)
.json()
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::IsActive)
.boolean()
.default(true)
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(InboundTemplates::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
// Index on name for searches
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_inbound_templates_name")
.table(InboundTemplates::Table)
.col(InboundTemplates::Name)
.to_owned(),
)
.await?;
// Index on protocol
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_inbound_templates_protocol")
.table(InboundTemplates::Table)
.col(InboundTemplates::Protocol)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_inbound_templates_protocol")
.to_owned(),
)
.await?;
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_inbound_templates_name")
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(InboundTemplates::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum InboundTemplates {
Table,
Id,
Name,
Description,
Protocol,
DefaultPort,
BaseSettings,
StreamSettings,
RequiresTls,
RequiresDomain,
Variables,
IsActive,
CreatedAt,
UpdatedAt,
}

View File

@@ -0,0 +1,136 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Servers::Table)
.if_not_exists()
.col(
ColumnDef::new(Servers::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Servers::Name)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(Servers::Hostname)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(Servers::GrpcPort)
.integer()
.default(2053)
.not_null(),
)
.col(
ColumnDef::new(Servers::ApiCredentials)
.text()
.null(),
)
.col(
ColumnDef::new(Servers::Status)
.string_len(50)
.default("unknown")
.not_null(),
)
.col(
ColumnDef::new(Servers::DefaultCertificateId)
.uuid()
.null(),
)
.col(
ColumnDef::new(Servers::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Servers::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
// Foreign key to certificates
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_servers_default_certificate")
.from(Servers::Table, Servers::DefaultCertificateId)
.to(Certificates::Table, Certificates::Id)
.on_delete(ForeignKeyAction::SetNull)
.to_owned(),
)
.await?;
// Index on hostname
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_servers_hostname")
.table(Servers::Table)
.col(Servers::Hostname)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_foreign_key(
ForeignKey::drop()
.name("fk_servers_default_certificate")
.table(Servers::Table)
.to_owned(),
)
.await?;
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_servers_hostname")
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Servers::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Servers {
Table,
Id,
Name,
Hostname,
GrpcPort,
ApiCredentials,
Status,
DefaultCertificateId,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Certificates {
Table,
Id,
}

View File

@@ -0,0 +1,195 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(ServerInbounds::Table)
.if_not_exists()
.col(
ColumnDef::new(ServerInbounds::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(ServerInbounds::ServerId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ServerInbounds::TemplateId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ServerInbounds::Tag)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(ServerInbounds::PortOverride)
.integer()
.null(),
)
.col(
ColumnDef::new(ServerInbounds::CertificateId)
.uuid()
.null(),
)
.col(
ColumnDef::new(ServerInbounds::VariableValues)
.json()
.not_null(),
)
.col(
ColumnDef::new(ServerInbounds::IsActive)
.boolean()
.default(true)
.not_null(),
)
.col(
ColumnDef::new(ServerInbounds::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(ServerInbounds::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
// Foreign keys
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_server_inbounds_server")
.from(ServerInbounds::Table, ServerInbounds::ServerId)
.to(Servers::Table, Servers::Id)
.on_delete(ForeignKeyAction::Cascade)
.to_owned(),
)
.await?;
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_server_inbounds_template")
.from(ServerInbounds::Table, ServerInbounds::TemplateId)
.to(InboundTemplates::Table, InboundTemplates::Id)
.on_delete(ForeignKeyAction::Restrict)
.to_owned(),
)
.await?;
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_server_inbounds_certificate")
.from(ServerInbounds::Table, ServerInbounds::CertificateId)
.to(Certificates::Table, Certificates::Id)
.on_delete(ForeignKeyAction::SetNull)
.to_owned(),
)
.await?;
// Unique constraint on server_id + tag
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_server_inbounds_server_tag")
.table(ServerInbounds::Table)
.col(ServerInbounds::ServerId)
.col(ServerInbounds::Tag)
.unique()
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_foreign_key(
ForeignKey::drop()
.name("fk_server_inbounds_certificate")
.table(ServerInbounds::Table)
.to_owned(),
)
.await?;
manager
.drop_foreign_key(
ForeignKey::drop()
.name("fk_server_inbounds_template")
.table(ServerInbounds::Table)
.to_owned(),
)
.await?;
manager
.drop_foreign_key(
ForeignKey::drop()
.name("fk_server_inbounds_server")
.table(ServerInbounds::Table)
.to_owned(),
)
.await?;
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_server_inbounds_server_tag")
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(ServerInbounds::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum ServerInbounds {
Table,
Id,
ServerId,
TemplateId,
Tag,
PortOverride,
CertificateId,
VariableValues,
IsActive,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Servers {
Table,
Id,
}
#[derive(DeriveIden)]
enum InboundTemplates {
Table,
Id,
}
#[derive(DeriveIden)]
enum Certificates {
Table,
Id,
}

View File

@@ -0,0 +1,196 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(UserAccess::Table)
.if_not_exists()
.col(
ColumnDef::new(UserAccess::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(UserAccess::UserId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(UserAccess::ServerId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(UserAccess::ServerInboundId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(UserAccess::XrayUserId)
.string()
.not_null(),
)
.col(
ColumnDef::new(UserAccess::XrayEmail)
.string()
.not_null(),
)
.col(
ColumnDef::new(UserAccess::Level)
.integer()
.not_null(),
)
.col(
ColumnDef::new(UserAccess::IsActive)
.boolean()
.not_null(),
)
.col(
ColumnDef::new(UserAccess::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(UserAccess::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_user_access_user_id")
.from(UserAccess::Table, UserAccess::UserId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_user_access_server_id")
.from(UserAccess::Table, UserAccess::ServerId)
.to(Servers::Table, Servers::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_user_access_server_inbound_id")
.from(UserAccess::Table, UserAccess::ServerInboundId)
.to(ServerInbounds::Table, ServerInbounds::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
// Create indexes separately
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_user_access_server_inbound")
.table(UserAccess::Table)
.col(UserAccess::ServerId)
.col(UserAccess::ServerInboundId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_user_access_user_server")
.table(UserAccess::Table)
.col(UserAccess::UserId)
.col(UserAccess::ServerId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_user_access_xray_email")
.table(UserAccess::Table)
.col(UserAccess::XrayEmail)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Drop indexes first
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_user_access_xray_email")
.to_owned(),
)
.await?;
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_user_access_user_server")
.to_owned(),
)
.await?;
manager
.drop_index(
Index::drop()
.if_exists()
.name("idx_user_access_server_inbound")
.to_owned(),
)
.await?;
// Drop table
manager
.drop_table(Table::drop().table(UserAccess::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum UserAccess {
Table,
Id,
UserId,
ServerId,
ServerInboundId,
XrayUserId,
XrayEmail,
Level,
IsActive,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
}
#[derive(DeriveIden)]
enum Servers {
Table,
Id,
}
#[derive(DeriveIden)]
enum ServerInbounds {
Table,
Id,
}

View File

@@ -0,0 +1,125 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(InboundUsers::Table)
.if_not_exists()
.col(
ColumnDef::new(InboundUsers::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(InboundUsers::ServerInboundId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(InboundUsers::Username)
.string()
.not_null(),
)
.col(
ColumnDef::new(InboundUsers::Email)
.string()
.not_null(),
)
.col(
ColumnDef::new(InboundUsers::XrayUserId)
.string()
.not_null(),
)
.col(
ColumnDef::new(InboundUsers::Level)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(InboundUsers::IsActive)
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(InboundUsers::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(InboundUsers::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_inbound_users_server_inbound")
.from(InboundUsers::Table, InboundUsers::ServerInboundId)
.to(ServerInbounds::Table, ServerInbounds::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
// Create unique constraint: one user per inbound
manager
.create_index(
Index::create()
.name("idx_inbound_users_unique_user_per_inbound")
.table(InboundUsers::Table)
.col(InboundUsers::ServerInboundId)
.col(InboundUsers::Username)
.unique()
.to_owned(),
)
.await?;
// Create index on email for faster lookups
manager
.create_index(
Index::create()
.name("idx_inbound_users_email")
.table(InboundUsers::Table)
.col(InboundUsers::Email)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(InboundUsers::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum InboundUsers {
Table,
Id,
ServerInboundId,
Username,
Email,
XrayUserId,
Level,
IsActive,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum ServerInbounds {
Table,
Id,
}

View File

@@ -0,0 +1,26 @@
use sea_orm_migration::prelude::*;
mod m20241201_000001_create_users_table;
mod m20241201_000002_create_certificates_table;
mod m20241201_000003_create_inbound_templates_table;
mod m20241201_000004_create_servers_table;
mod m20241201_000005_create_server_inbounds_table;
mod m20241201_000006_create_user_access_table;
mod m20241201_000007_create_inbound_users_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20241201_000001_create_users_table::Migration),
Box::new(m20241201_000002_create_certificates_table::Migration),
Box::new(m20241201_000003_create_inbound_templates_table::Migration),
Box::new(m20241201_000004_create_servers_table::Migration),
Box::new(m20241201_000005_create_server_inbounds_table::Migration),
Box::new(m20241201_000006_create_user_access_table::Migration),
Box::new(m20241201_000007_create_inbound_users_table::Migration),
]
}
}

161
src/database/mod.rs Normal file
View File

@@ -0,0 +1,161 @@
use anyhow::Result;
use sea_orm::{Database, DatabaseConnection, ConnectOptions, Statement, DatabaseBackend, ConnectionTrait};
use sea_orm_migration::MigratorTrait;
use std::time::Duration;
use tracing::{info, warn};
use crate::config::DatabaseConfig;
pub mod entities;
pub mod migrations;
pub mod repository;
use migrations::Migrator;
/// Database connection and management
#[derive(Clone)]
pub struct DatabaseManager {
connection: DatabaseConnection,
}
impl DatabaseManager {
/// Create a new database connection
pub async fn new(config: &DatabaseConfig) -> Result<Self> {
info!("Connecting to database...");
// URL-encode the connection string to handle special characters in passwords
let encoded_url = Self::encode_database_url(&config.url)?;
let mut opt = ConnectOptions::new(&encoded_url);
opt.max_connections(config.max_connections)
.min_connections(1)
.connect_timeout(Duration::from_secs(config.connection_timeout))
.acquire_timeout(Duration::from_secs(config.connection_timeout))
.idle_timeout(Duration::from_secs(600))
.max_lifetime(Duration::from_secs(3600))
.sqlx_logging(tracing::level_enabled!(tracing::Level::DEBUG))
.sqlx_logging_level(log::LevelFilter::Debug);
let connection = Database::connect(opt).await?;
info!("Database connection established successfully");
let manager = Self { connection };
// Run migrations if auto_migrate is enabled
if config.auto_migrate {
manager.migrate().await?;
}
Ok(manager)
}
/// Get database connection
pub fn connection(&self) -> &DatabaseConnection {
&self.connection
}
/// Run database migrations
pub async fn migrate(&self) -> Result<()> {
info!("Running database migrations...");
match Migrator::up(&self.connection, None).await {
Ok(_) => {
info!("Database migrations completed successfully");
Ok(())
}
Err(e) => {
warn!("Migration error: {}", e);
Err(e.into())
}
}
}
/// Check database connection health
pub async fn health_check(&self) -> Result<bool> {
let stmt = Statement::from_string(DatabaseBackend::Postgres, "SELECT 1".to_owned());
match self.connection.execute(stmt).await {
Ok(_) => Ok(true),
Err(e) => {
warn!("Database health check failed: {}", e);
Ok(false)
}
}
}
/// Get database schema information
pub async fn get_schema_version(&self) -> Result<Option<String>> {
// This would typically query a migrations table
// For now, we'll just return a placeholder
Ok(Some("1.0.0".to_string()))
}
/// Encode database URL to handle special characters in passwords
fn encode_database_url(url: &str) -> Result<String> {
// Parse URL manually to handle special characters in password
if let Some(at_pos) = url.rfind('@') {
if let Some(_colon_pos) = url[..at_pos].rfind(':') {
if let Some(scheme_end) = url.find("://") {
let scheme = &url[..scheme_end + 3];
let user_pass = &url[scheme_end + 3..at_pos];
let host_db = &url[at_pos..];
if let Some(user_colon) = user_pass.find(':') {
let user = &user_pass[..user_colon];
let password = &user_pass[user_colon + 1..];
// URL-encode the password part only
let encoded_password = urlencoding::encode(password);
let encoded_url = format!("{}{}:{}{}", scheme, user, encoded_password, host_db);
return Ok(encoded_url);
}
}
}
}
// If parsing fails, return original URL
Ok(url.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::DatabaseConfig;
#[test]
fn test_encode_database_url() {
let url_with_special_chars = "postgresql://user:pass#word@localhost:5432/db";
let encoded = DatabaseManager::encode_database_url(url_with_special_chars).unwrap();
assert_eq!(encoded, "postgresql://user:pass%23word@localhost:5432/db");
let normal_url = "postgresql://user:password@localhost:5432/db";
let encoded_normal = DatabaseManager::encode_database_url(normal_url).unwrap();
assert_eq!(encoded_normal, "postgresql://user:password@localhost:5432/db");
}
#[tokio::test]
async fn test_database_connection() {
// This test requires a running PostgreSQL database
// Skip in CI or when database is not available
if std::env::var("DATABASE_URL").is_err() {
return;
}
let config = DatabaseConfig {
url: std::env::var("DATABASE_URL").unwrap(),
max_connections: 5,
connection_timeout: 30,
auto_migrate: false,
};
let db = DatabaseManager::new(&config).await;
assert!(db.is_ok());
if let Ok(db) = db {
let health = db.health_check().await;
assert!(health.is_ok());
}
}
}

View File

@@ -0,0 +1,75 @@
use sea_orm::*;
use crate::database::entities::{certificate, prelude::*};
use anyhow::Result;
use uuid::Uuid;
#[derive(Clone)]
pub struct CertificateRepository {
db: DatabaseConnection,
}
impl CertificateRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
pub async fn create(&self, cert_data: certificate::CreateCertificateDto) -> Result<certificate::Model> {
let cert = certificate::ActiveModel::from(cert_data);
let result = Certificate::insert(cert).exec(&self.db).await?;
Certificate::find_by_id(result.last_insert_id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created certificate"))
}
pub async fn find_all(&self) -> Result<Vec<certificate::Model>> {
Ok(Certificate::find().all(&self.db).await?)
}
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<certificate::Model>> {
Ok(Certificate::find_by_id(id).one(&self.db).await?)
}
#[allow(dead_code)]
pub async fn find_by_domain(&self, domain: &str) -> Result<Vec<certificate::Model>> {
Ok(Certificate::find()
.filter(certificate::Column::Domain.eq(domain))
.all(&self.db)
.await?)
}
#[allow(dead_code)]
pub async fn find_by_type(&self, cert_type: &str) -> Result<Vec<certificate::Model>> {
Ok(Certificate::find()
.filter(certificate::Column::CertType.eq(cert_type))
.all(&self.db)
.await?)
}
pub async fn update(&self, id: Uuid, cert_data: certificate::UpdateCertificateDto) -> Result<certificate::Model> {
let cert = Certificate::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Certificate not found"))?;
let updated_cert = cert.apply_update(cert_data);
Ok(updated_cert.update(&self.db).await?)
}
pub async fn delete(&self, id: Uuid) -> Result<bool> {
let result = Certificate::delete_by_id(id).exec(&self.db).await?;
Ok(result.rows_affected > 0)
}
pub async fn find_expiring_soon(&self, days: i64) -> Result<Vec<certificate::Model>> {
let threshold = chrono::Utc::now() + chrono::Duration::days(days);
Ok(Certificate::find()
.filter(certificate::Column::ExpiresAt.lt(threshold))
.all(&self.db)
.await?)
}
}

View File

@@ -0,0 +1,65 @@
use sea_orm::*;
use crate::database::entities::{inbound_template, prelude::*};
use anyhow::Result;
use uuid::Uuid;
#[derive(Clone)]
pub struct InboundTemplateRepository {
db: DatabaseConnection,
}
#[allow(dead_code)]
impl InboundTemplateRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
pub async fn create(&self, template_data: inbound_template::CreateInboundTemplateDto) -> Result<inbound_template::Model> {
let template = inbound_template::ActiveModel::from(template_data);
let result = InboundTemplate::insert(template).exec(&self.db).await?;
InboundTemplate::find_by_id(result.last_insert_id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created template"))
}
pub async fn find_all(&self) -> Result<Vec<inbound_template::Model>> {
Ok(InboundTemplate::find().all(&self.db).await?)
}
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<inbound_template::Model>> {
Ok(InboundTemplate::find_by_id(id).one(&self.db).await?)
}
pub async fn find_by_name(&self, name: &str) -> Result<Option<inbound_template::Model>> {
Ok(InboundTemplate::find()
.filter(inbound_template::Column::Name.eq(name))
.one(&self.db)
.await?)
}
pub async fn find_by_protocol(&self, protocol: &str) -> Result<Vec<inbound_template::Model>> {
Ok(InboundTemplate::find()
.filter(inbound_template::Column::Protocol.eq(protocol))
.all(&self.db)
.await?)
}
pub async fn update(&self, id: Uuid, template_data: inbound_template::UpdateInboundTemplateDto) -> Result<inbound_template::Model> {
let template = InboundTemplate::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Template not found"))?;
let updated_template = template.apply_update(template_data);
Ok(updated_template.update(&self.db).await?)
}
pub async fn delete(&self, id: Uuid) -> Result<bool> {
let result = InboundTemplate::delete_by_id(id).exec(&self.db).await?;
Ok(result.rows_affected > 0)
}
}

View File

@@ -0,0 +1,132 @@
use anyhow::Result;
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter, Set};
use uuid::Uuid;
use crate::database::entities::inbound_users::{
Entity, Model, ActiveModel, CreateInboundUserDto, UpdateInboundUserDto, Column
};
pub struct InboundUsersRepository {
db: DatabaseConnection,
}
impl InboundUsersRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
pub async fn find_all(&self) -> Result<Vec<Model>> {
let users = Entity::find().all(&self.db).await?;
Ok(users)
}
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
let user = Entity::find_by_id(id).one(&self.db).await?;
Ok(user)
}
/// Find all users for a specific inbound
pub async fn find_by_inbound_id(&self, inbound_id: Uuid) -> Result<Vec<Model>> {
let users = Entity::find()
.filter(Column::ServerInboundId.eq(inbound_id))
.all(&self.db)
.await?;
Ok(users)
}
/// Find active users for a specific inbound
pub async fn find_active_by_inbound_id(&self, inbound_id: Uuid) -> Result<Vec<Model>> {
let users = Entity::find()
.filter(Column::ServerInboundId.eq(inbound_id))
.filter(Column::IsActive.eq(true))
.all(&self.db)
.await?;
Ok(users)
}
/// Find user by username and inbound (for uniqueness check)
pub async fn find_by_username_and_inbound(&self, username: &str, inbound_id: Uuid) -> Result<Option<Model>> {
let user = Entity::find()
.filter(Column::Username.eq(username))
.filter(Column::ServerInboundId.eq(inbound_id))
.one(&self.db)
.await?;
Ok(user)
}
/// Find user by email
pub async fn find_by_email(&self, email: &str) -> Result<Option<Model>> {
let user = Entity::find()
.filter(Column::Email.eq(email))
.one(&self.db)
.await?;
Ok(user)
}
pub async fn create(&self, dto: CreateInboundUserDto) -> Result<Model> {
let active_model: ActiveModel = dto.into();
let user = active_model.insert(&self.db).await?;
Ok(user)
}
pub async fn update(&self, id: Uuid, dto: UpdateInboundUserDto) -> Result<Option<Model>> {
let user = match self.find_by_id(id).await? {
Some(user) => user,
None => return Ok(None),
};
let updated_model = user.apply_update(dto);
let updated_user = updated_model.update(&self.db).await?;
Ok(Some(updated_user))
}
pub async fn delete(&self, id: Uuid) -> Result<bool> {
let result = Entity::delete_by_id(id).exec(&self.db).await?;
Ok(result.rows_affected > 0)
}
/// Enable user (set is_active = true)
pub async fn enable(&self, id: Uuid) -> Result<Option<Model>> {
let user = match self.find_by_id(id).await? {
Some(user) => user,
None => return Ok(None),
};
let mut active_model: ActiveModel = user.into();
active_model.is_active = Set(true);
active_model.updated_at = Set(chrono::Utc::now());
let updated_user = active_model.update(&self.db).await?;
Ok(Some(updated_user))
}
/// Disable user (set is_active = false)
pub async fn disable(&self, id: Uuid) -> Result<Option<Model>> {
let user = match self.find_by_id(id).await? {
Some(user) => user,
None => return Ok(None),
};
let mut active_model: ActiveModel = user.into();
active_model.is_active = Set(false);
active_model.updated_at = Set(chrono::Utc::now());
let updated_user = active_model.update(&self.db).await?;
Ok(Some(updated_user))
}
/// Remove all users for a specific inbound (when inbound is deleted)
pub async fn remove_all_for_inbound(&self, inbound_id: Uuid) -> Result<u64> {
let result = Entity::delete_many()
.filter(Column::ServerInboundId.eq(inbound_id))
.exec(&self.db)
.await?;
Ok(result.rows_affected)
}
/// Check if username already exists on this inbound
pub async fn username_exists_on_inbound(&self, username: &str, inbound_id: Uuid) -> Result<bool> {
let exists = self.find_by_username_and_inbound(username, inbound_id).await?;
Ok(exists.is_some())
}
}

View File

@@ -0,0 +1,15 @@
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 use user::UserRepository;
pub use certificate::CertificateRepository;
pub use inbound_template::InboundTemplateRepository;
pub use server::ServerRepository;
pub use server_inbound::ServerInboundRepository;
pub use user_access::UserAccessRepository;
pub use inbound_users::InboundUsersRepository;

View File

@@ -0,0 +1,79 @@
use sea_orm::*;
use crate::database::entities::{server, prelude::*};
use anyhow::Result;
use uuid::Uuid;
#[derive(Clone)]
pub struct ServerRepository {
db: DatabaseConnection,
}
#[allow(dead_code)]
impl ServerRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
pub async fn create(&self, server_data: server::CreateServerDto) -> Result<server::Model> {
let server = server::ActiveModel::from(server_data);
let result = Server::insert(server).exec(&self.db).await?;
Server::find_by_id(result.last_insert_id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created server"))
}
pub async fn find_all(&self) -> Result<Vec<server::Model>> {
Ok(Server::find().all(&self.db).await?)
}
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<server::Model>> {
Ok(Server::find_by_id(id).one(&self.db).await?)
}
pub async fn find_by_name(&self, name: &str) -> Result<Option<server::Model>> {
Ok(Server::find()
.filter(server::Column::Name.eq(name))
.one(&self.db)
.await?)
}
pub async fn find_by_hostname(&self, hostname: &str) -> Result<Option<server::Model>> {
Ok(Server::find()
.filter(server::Column::Hostname.eq(hostname))
.one(&self.db)
.await?)
}
pub async fn find_by_status(&self, status: &str) -> Result<Vec<server::Model>> {
Ok(Server::find()
.filter(server::Column::Status.eq(status))
.all(&self.db)
.await?)
}
pub async fn update(&self, id: Uuid, server_data: server::UpdateServerDto) -> Result<server::Model> {
let server = Server::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Server not found"))?;
let updated_server = server.apply_update(server_data);
Ok(updated_server.update(&self.db).await?)
}
pub async fn delete(&self, id: Uuid) -> Result<bool> {
let result = Server::delete_by_id(id).exec(&self.db).await?;
Ok(result.rows_affected > 0)
}
pub async fn get_grpc_endpoint(&self, id: Uuid) -> Result<String> {
let server = self.find_by_id(id).await?
.ok_or_else(|| anyhow::anyhow!("Server not found"))?;
Ok(format!("{}:{}", server.hostname, server.grpc_port))
}
}

View File

@@ -0,0 +1,159 @@
use sea_orm::*;
use crate::database::entities::{server_inbound, prelude::*};
use anyhow::Result;
use uuid::Uuid;
#[derive(Clone)]
pub struct ServerInboundRepository {
db: DatabaseConnection,
}
#[allow(dead_code)]
impl ServerInboundRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
pub async fn create(&self, server_id: Uuid, inbound_data: server_inbound::CreateServerInboundDto) -> Result<server_inbound::Model> {
let mut inbound: server_inbound::ActiveModel = inbound_data.into();
inbound.id = Set(Uuid::new_v4());
inbound.server_id = Set(server_id);
inbound.created_at = Set(chrono::Utc::now());
inbound.updated_at = Set(chrono::Utc::now());
let result = ServerInbound::insert(inbound).exec(&self.db).await?;
ServerInbound::find_by_id(result.last_insert_id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created server inbound"))
}
pub async fn create_with_protocol(&self, server_id: Uuid, inbound_data: server_inbound::CreateServerInboundDto, protocol: &str) -> Result<server_inbound::Model> {
let mut inbound: server_inbound::ActiveModel = inbound_data.into();
inbound.id = Set(Uuid::new_v4());
inbound.server_id = Set(server_id);
inbound.created_at = Set(chrono::Utc::now());
inbound.updated_at = Set(chrono::Utc::now());
// Override tag with protocol prefix
let id = inbound.id.as_ref();
inbound.tag = Set(format!("{}-inbound-{}", protocol, id));
let result = ServerInbound::insert(inbound).exec(&self.db).await?;
ServerInbound::find_by_id(result.last_insert_id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created server inbound"))
}
pub async fn find_all(&self) -> Result<Vec<server_inbound::Model>> {
Ok(ServerInbound::find().all(&self.db).await?)
}
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<server_inbound::Model>> {
Ok(ServerInbound::find_by_id(id).one(&self.db).await?)
}
pub async fn find_by_server_id(&self, server_id: Uuid) -> Result<Vec<server_inbound::Model>> {
Ok(ServerInbound::find()
.filter(server_inbound::Column::ServerId.eq(server_id))
.all(&self.db)
.await?)
}
pub async fn find_by_server_id_with_template(&self, server_id: Uuid) -> Result<Vec<server_inbound::ServerInboundResponse>> {
use crate::database::entities::{inbound_template, certificate};
let inbounds = ServerInbound::find()
.filter(server_inbound::Column::ServerId.eq(server_id))
.all(&self.db)
.await?;
let mut responses = Vec::new();
for inbound in inbounds {
let mut response = server_inbound::ServerInboundResponse::from(inbound.clone());
// Load template information
if let Ok(Some(template)) = InboundTemplate::find_by_id(inbound.template_id).one(&self.db).await {
response.template_name = Some(template.name);
}
// Load certificate information
if let Some(cert_id) = inbound.certificate_id {
if let Ok(Some(certificate)) = Certificate::find_by_id(cert_id).one(&self.db).await {
response.certificate_name = Some(certificate.domain);
}
}
responses.push(response);
}
Ok(responses)
}
pub async fn find_by_template_id(&self, template_id: Uuid) -> Result<Vec<server_inbound::Model>> {
Ok(ServerInbound::find()
.filter(server_inbound::Column::TemplateId.eq(template_id))
.all(&self.db)
.await?)
}
pub async fn find_by_tag(&self, tag: &str) -> Result<Option<server_inbound::Model>> {
Ok(ServerInbound::find()
.filter(server_inbound::Column::Tag.eq(tag))
.one(&self.db)
.await?)
}
pub async fn find_active_by_server(&self, server_id: Uuid) -> Result<Vec<server_inbound::Model>> {
Ok(ServerInbound::find()
.filter(server_inbound::Column::ServerId.eq(server_id))
.filter(server_inbound::Column::IsActive.eq(true))
.all(&self.db)
.await?)
}
pub async fn update(&self, id: Uuid, inbound_data: server_inbound::UpdateServerInboundDto) -> Result<server_inbound::Model> {
let inbound = ServerInbound::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
let updated_inbound = inbound.apply_update(inbound_data);
Ok(updated_inbound.update(&self.db).await?)
}
pub async fn delete(&self, id: Uuid) -> Result<bool> {
let result = ServerInbound::delete_by_id(id).exec(&self.db).await?;
Ok(result.rows_affected > 0)
}
pub async fn activate(&self, id: Uuid) -> Result<server_inbound::Model> {
let inbound = ServerInbound::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
let mut inbound: server_inbound::ActiveModel = inbound.into();
inbound.is_active = Set(true);
inbound.updated_at = Set(chrono::Utc::now());
Ok(inbound.update(&self.db).await?)
}
pub async fn deactivate(&self, id: Uuid) -> Result<server_inbound::Model> {
let inbound = ServerInbound::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
let mut inbound: server_inbound::ActiveModel = inbound.into();
inbound.is_active = Set(false);
inbound.updated_at = Set(chrono::Utc::now());
Ok(inbound.update(&self.db).await?)
}
}

View File

@@ -0,0 +1,157 @@
use anyhow::Result;
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait};
use uuid::Uuid;
use crate::database::entities::user::{Entity as User, Column, Model, ActiveModel, CreateUserDto, UpdateUserDto};
pub struct UserRepository {
db: DatabaseConnection,
}
impl UserRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
/// Get all users with pagination
pub async fn get_all(&self, page: u64, per_page: u64) -> Result<Vec<Model>> {
let users = User::find()
.order_by_desc(Column::CreatedAt)
.paginate(&self.db, per_page)
.fetch_page(page.saturating_sub(1))
.await?;
Ok(users)
}
/// Get user by ID
pub async fn get_by_id(&self, id: Uuid) -> Result<Option<Model>> {
let user = User::find_by_id(id).one(&self.db).await?;
Ok(user)
}
/// Get user by telegram ID
pub async fn get_by_telegram_id(&self, telegram_id: i64) -> Result<Option<Model>> {
let user = User::find()
.filter(Column::TelegramId.eq(telegram_id))
.one(&self.db)
.await?;
Ok(user)
}
/// Search users by name
pub async fn search_by_name(&self, query: &str, page: u64, per_page: u64) -> Result<Vec<Model>> {
let users = User::find()
.filter(Column::Name.contains(query))
.order_by_desc(Column::CreatedAt)
.paginate(&self.db, per_page)
.fetch_page(page.saturating_sub(1))
.await?;
Ok(users)
}
/// Create a new user
pub async fn create(&self, dto: CreateUserDto) -> Result<Model> {
let active_model: ActiveModel = dto.into();
let user = User::insert(active_model).exec_with_returning(&self.db).await?;
Ok(user)
}
/// Update user by ID
pub async fn update(&self, id: Uuid, dto: UpdateUserDto) -> Result<Option<Model>> {
if let Some(user) = self.get_by_id(id).await? {
let active_model = user.apply_update(dto);
User::update(active_model).exec(&self.db).await?;
// Fetch the updated user
self.get_by_id(id).await
} else {
Ok(None)
}
}
/// Delete user by ID
pub async fn delete(&self, id: Uuid) -> Result<bool> {
let result = User::delete_by_id(id).exec(&self.db).await?;
Ok(result.rows_affected > 0)
}
/// Get total count of users
pub async fn count(&self) -> Result<u64> {
let count = User::find().count(&self.db).await?;
Ok(count)
}
/// Check if telegram ID is already used
pub async fn telegram_id_exists(&self, telegram_id: i64) -> Result<bool> {
let count = User::find()
.filter(Column::TelegramId.eq(telegram_id))
.count(&self.db)
.await?;
Ok(count > 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::DatabaseManager;
use crate::config::DatabaseConfig;
async fn setup_test_db() -> Result<UserRepository> {
let config = DatabaseConfig {
url: std::env::var("DATABASE_URL").unwrap_or_else(|_|
"sqlite::memory:".to_string()
),
max_connections: 5,
connection_timeout: 30,
auto_migrate: true,
};
let db_manager = DatabaseManager::new(&config).await?;
Ok(UserRepository::new(db_manager.connection().clone()))
}
#[tokio::test]
async fn test_user_crud() {
let repo = match setup_test_db().await {
Ok(repo) => repo,
Err(_) => return, // Skip test if no database available
};
// Create user
let create_dto = CreateUserDto {
name: "Test User".to_string(),
comment: Some("Test comment".to_string()),
telegram_id: Some(123456789),
};
let created_user = repo.create(create_dto).await.unwrap();
assert_eq!(created_user.name, "Test User");
assert_eq!(created_user.telegram_id, Some(123456789));
// Get by ID
let fetched_user = repo.get_by_id(created_user.id).await.unwrap();
assert!(fetched_user.is_some());
assert_eq!(fetched_user.unwrap().name, "Test User");
// Update user
let update_dto = UpdateUserDto {
name: Some("Updated User".to_string()),
comment: None,
telegram_id: None,
};
let updated_user = repo.update(created_user.id, update_dto).await.unwrap();
assert!(updated_user.is_some());
assert_eq!(updated_user.unwrap().name, "Updated User");
// Delete user
let deleted = repo.delete(created_user.id).await.unwrap();
assert!(deleted);
// Verify deletion
let deleted_user = repo.get_by_id(created_user.id).await.unwrap();
assert!(deleted_user.is_none());
}
}

View File

@@ -0,0 +1,118 @@
use sea_orm::*;
use uuid::Uuid;
use anyhow::Result;
use crate::database::entities::user_access::{self, Entity as UserAccess, Model, ActiveModel, CreateUserAccessDto, UpdateUserAccessDto};
pub struct UserAccessRepository {
db: DatabaseConnection,
}
impl UserAccessRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
/// Find all user access records
pub async fn find_all(&self) -> Result<Vec<Model>> {
let records = UserAccess::find().all(&self.db).await?;
Ok(records)
}
/// Find user access by ID
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
let record = UserAccess::find_by_id(id).one(&self.db).await?;
Ok(record)
}
/// Find user access by user ID
pub async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<Model>> {
let records = UserAccess::find()
.filter(user_access::Column::UserId.eq(user_id))
.all(&self.db)
.await?;
Ok(records)
}
/// Find user access by server and inbound
pub async fn find_by_server_inbound(&self, server_id: Uuid, server_inbound_id: Uuid) -> Result<Vec<Model>> {
let records = UserAccess::find()
.filter(user_access::Column::ServerId.eq(server_id))
.filter(user_access::Column::ServerInboundId.eq(server_inbound_id))
.all(&self.db)
.await?;
Ok(records)
}
/// Find active user access for specific user, server and inbound
pub async fn find_active_access(&self, user_id: Uuid, server_id: Uuid, server_inbound_id: Uuid) -> Result<Option<Model>> {
let record = UserAccess::find()
.filter(user_access::Column::UserId.eq(user_id))
.filter(user_access::Column::ServerId.eq(server_id))
.filter(user_access::Column::ServerInboundId.eq(server_inbound_id))
.filter(user_access::Column::IsActive.eq(true))
.one(&self.db)
.await?;
Ok(record)
}
/// Create new user access
pub async fn create(&self, dto: CreateUserAccessDto) -> Result<Model> {
let active_model: ActiveModel = dto.into();
let model = active_model.insert(&self.db).await?;
Ok(model)
}
/// Update user access
pub async fn update(&self, id: Uuid, dto: UpdateUserAccessDto) -> Result<Option<Model>> {
let existing = match self.find_by_id(id).await? {
Some(model) => model,
None => return Ok(None),
};
let active_model = existing.apply_update(dto);
let updated = active_model.update(&self.db).await?;
Ok(Some(updated))
}
/// Delete user access
pub async fn delete(&self, id: Uuid) -> Result<bool> {
let result = UserAccess::delete_by_id(id).exec(&self.db).await?;
Ok(result.rows_affected > 0)
}
/// Enable user access (set is_active = true)
pub async fn enable(&self, id: Uuid) -> Result<Option<Model>> {
self.update(id, UpdateUserAccessDto {
is_active: Some(true),
level: None,
}).await
}
/// Disable user access (set is_active = false)
pub async fn disable(&self, id: Uuid) -> Result<Option<Model>> {
self.update(id, UpdateUserAccessDto {
is_active: Some(false),
level: None,
}).await
}
/// Get all active access for a user
pub async fn find_active_for_user(&self, user_id: Uuid) -> Result<Vec<Model>> {
let records = UserAccess::find()
.filter(user_access::Column::UserId.eq(user_id))
.filter(user_access::Column::IsActive.eq(true))
.all(&self.db)
.await?;
Ok(records)
}
/// Remove all access for a specific server inbound
pub async fn remove_all_for_inbound(&self, server_inbound_id: Uuid) -> Result<u64> {
let result = UserAccess::delete_many()
.filter(user_access::Column::ServerInboundId.eq(server_inbound_id))
.exec(&self.db)
.await?;
Ok(result.rows_affected)
}
}