mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-25 17:59:08 +00:00
Added usermanagement in TG admin
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -5158,7 +5158,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xray-admin"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "xray-admin"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -69,4 +69,4 @@ pem = "3.0" # PEM format support
|
||||
teloxide = { version = "0.13", features = ["macros"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
tempfile = "3.0"
|
||||
|
||||
@@ -51,13 +51,17 @@ mod tests {
|
||||
fn test_args_parsing() {
|
||||
let args = Args::try_parse_from(&[
|
||||
"xray-admin",
|
||||
"--config", "test.toml",
|
||||
"--port", "9090",
|
||||
"--log-level", "debug"
|
||||
]).unwrap();
|
||||
"--config",
|
||||
"test.toml",
|
||||
"--port",
|
||||
"9090",
|
||||
"--log-level",
|
||||
"debug",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(args.config, Some(PathBuf::from("test.toml")));
|
||||
assert_eq!(args.port, Some(9090));
|
||||
assert_eq!(args.log_level, Some("debug".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,21 +43,24 @@ impl EnvVars {
|
||||
/// Get database URL from environment
|
||||
#[allow(dead_code)]
|
||||
pub fn database_url() -> Option<String> {
|
||||
env::var("DATABASE_URL").ok()
|
||||
env::var("DATABASE_URL")
|
||||
.ok()
|
||||
.or_else(|| env::var("XRAY_ADMIN__DATABASE__URL").ok())
|
||||
}
|
||||
|
||||
/// Get telegram bot token from environment
|
||||
#[allow(dead_code)]
|
||||
pub fn telegram_token() -> Option<String> {
|
||||
env::var("TELEGRAM_BOT_TOKEN").ok()
|
||||
env::var("TELEGRAM_BOT_TOKEN")
|
||||
.ok()
|
||||
.or_else(|| env::var("XRAY_ADMIN__TELEGRAM__BOT_TOKEN").ok())
|
||||
}
|
||||
|
||||
/// Get JWT secret from environment
|
||||
#[allow(dead_code)]
|
||||
pub fn jwt_secret() -> Option<String> {
|
||||
env::var("JWT_SECRET").ok()
|
||||
env::var("JWT_SECRET")
|
||||
.ok()
|
||||
.or_else(|| env::var("XRAY_ADMIN__WEB__JWT_SECRET").ok())
|
||||
}
|
||||
|
||||
@@ -66,14 +69,29 @@ impl EnvVars {
|
||||
tracing::debug!("Environment information:");
|
||||
tracing::debug!(" RUST_ENV: {:?}", env::var("RUST_ENV"));
|
||||
tracing::debug!(" ENVIRONMENT: {:?}", env::var("ENVIRONMENT"));
|
||||
tracing::debug!(" DATABASE_URL: {}",
|
||||
if env::var("DATABASE_URL").is_ok() { "set" } else { "not set" }
|
||||
tracing::debug!(
|
||||
" DATABASE_URL: {}",
|
||||
if env::var("DATABASE_URL").is_ok() {
|
||||
"set"
|
||||
} else {
|
||||
"not set"
|
||||
}
|
||||
);
|
||||
tracing::debug!(" TELEGRAM_BOT_TOKEN: {}",
|
||||
if env::var("TELEGRAM_BOT_TOKEN").is_ok() { "set" } else { "not set" }
|
||||
tracing::debug!(
|
||||
" TELEGRAM_BOT_TOKEN: {}",
|
||||
if env::var("TELEGRAM_BOT_TOKEN").is_ok() {
|
||||
"set"
|
||||
} else {
|
||||
"not set"
|
||||
}
|
||||
);
|
||||
tracing::debug!(" JWT_SECRET: {}",
|
||||
if env::var("JWT_SECRET").is_ok() { "set" } else { "not set" }
|
||||
tracing::debug!(
|
||||
" JWT_SECRET: {}",
|
||||
if env::var("JWT_SECRET").is_ok() {
|
||||
"set"
|
||||
} else {
|
||||
"not set"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -101,4 +119,4 @@ mod tests {
|
||||
|
||||
env::remove_var("RUST_ENV");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@ impl ConfigFile {
|
||||
pub fn load_toml<P: AsRef<Path>>(path: P) -> Result<AppConfig> {
|
||||
let content = fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
|
||||
|
||||
let config: AppConfig = toml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse TOML config file: {}", path.as_ref().display()))?;
|
||||
|
||||
|
||||
let config: AppConfig = toml::from_str(&content).with_context(|| {
|
||||
format!(
|
||||
"Failed to parse TOML config file: {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -25,10 +29,14 @@ impl ConfigFile {
|
||||
pub fn load_yaml<P: AsRef<Path>>(path: P) -> Result<AppConfig> {
|
||||
let content = fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
|
||||
|
||||
let config: AppConfig = serde_yaml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse YAML config file: {}", path.as_ref().display()))?;
|
||||
|
||||
|
||||
let config: AppConfig = serde_yaml::from_str(&content).with_context(|| {
|
||||
format!(
|
||||
"Failed to parse YAML config file: {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -36,17 +44,21 @@ impl ConfigFile {
|
||||
pub fn load_json<P: AsRef<Path>>(path: P) -> Result<AppConfig> {
|
||||
let content = fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
|
||||
|
||||
let config: AppConfig = serde_json::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse JSON config file: {}", path.as_ref().display()))?;
|
||||
|
||||
|
||||
let config: AppConfig = serde_json::from_str(&content).with_context(|| {
|
||||
format!(
|
||||
"Failed to parse JSON config file: {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Auto-detect format and load configuration file
|
||||
pub fn load_auto<P: AsRef<Path>>(path: P) -> Result<AppConfig> {
|
||||
let path = path.as_ref();
|
||||
|
||||
|
||||
match path.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("toml") => Self::load_toml(path),
|
||||
Some("yaml") | Some("yml") => Self::load_yaml(path),
|
||||
@@ -68,41 +80,45 @@ impl ConfigFile {
|
||||
|
||||
/// Save configuration to TOML file
|
||||
pub fn save_toml<P: AsRef<Path>>(config: &AppConfig, path: P) -> Result<()> {
|
||||
let content = toml::to_string_pretty(config)
|
||||
.context("Failed to serialize config to TOML")?;
|
||||
|
||||
let content =
|
||||
toml::to_string_pretty(config).context("Failed to serialize config to TOML")?;
|
||||
|
||||
fs::write(&path, content)
|
||||
.with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save configuration to YAML file
|
||||
pub fn save_yaml<P: AsRef<Path>>(config: &AppConfig, path: P) -> Result<()> {
|
||||
let content = serde_yaml::to_string(config)
|
||||
.context("Failed to serialize config to YAML")?;
|
||||
|
||||
let content =
|
||||
serde_yaml::to_string(config).context("Failed to serialize config to YAML")?;
|
||||
|
||||
fs::write(&path, content)
|
||||
.with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save configuration to JSON file
|
||||
pub fn save_json<P: AsRef<Path>>(config: &AppConfig, path: P) -> Result<()> {
|
||||
let content = serde_json::to_string_pretty(config)
|
||||
.context("Failed to serialize config to JSON")?;
|
||||
|
||||
let content =
|
||||
serde_json::to_string_pretty(config).context("Failed to serialize config to JSON")?;
|
||||
|
||||
fs::write(&path, content)
|
||||
.with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if config file exists and is readable
|
||||
pub fn exists_and_readable<P: AsRef<Path>>(path: P) -> bool {
|
||||
let path = path.as_ref();
|
||||
path.exists() && path.is_file() && fs::metadata(path).map(|m| !m.permissions().readonly()).unwrap_or(false)
|
||||
path.exists()
|
||||
&& path.is_file()
|
||||
&& fs::metadata(path)
|
||||
.map(|m| !m.permissions().readonly())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Find default config file in common locations
|
||||
@@ -140,26 +156,29 @@ mod tests {
|
||||
fn test_save_and_load_toml() -> Result<()> {
|
||||
let config = AppConfig::default();
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
|
||||
|
||||
ConfigFile::save_toml(&config, temp_file.path())?;
|
||||
let loaded_config = ConfigFile::load_toml(temp_file.path())?;
|
||||
|
||||
|
||||
assert_eq!(config.web.port, loaded_config.web.port);
|
||||
assert_eq!(config.database.max_connections, loaded_config.database.max_connections);
|
||||
|
||||
assert_eq!(
|
||||
config.database.max_connections,
|
||||
loaded_config.database.max_connections
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_detect_format() -> Result<()> {
|
||||
let config = AppConfig::default();
|
||||
|
||||
|
||||
// Test with .toml extension
|
||||
let temp_file = NamedTempFile::with_suffix(".toml")?;
|
||||
ConfigFile::save_toml(&config, temp_file.path())?;
|
||||
let loaded_config = ConfigFile::load_auto(temp_file.path())?;
|
||||
assert_eq!(config.web.port, loaded_config.web.port);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ impl AppConfig {
|
||||
/// 4. Default values (lowest)
|
||||
pub fn load() -> Result<Self> {
|
||||
let args = args::parse_args();
|
||||
|
||||
|
||||
let mut builder = config::Config::builder()
|
||||
// Start with defaults
|
||||
.add_source(config::Config::try_from(&AppConfig::default())?);
|
||||
@@ -163,7 +163,7 @@ impl AppConfig {
|
||||
builder = builder.add_source(
|
||||
config::Environment::with_prefix("XRAY_ADMIN")
|
||||
.separator("__")
|
||||
.try_parsing(true)
|
||||
.try_parsing(true),
|
||||
);
|
||||
|
||||
// Override with command line arguments
|
||||
@@ -184,10 +184,10 @@ impl AppConfig {
|
||||
}
|
||||
|
||||
let config: AppConfig = builder.build()?.try_deserialize()?;
|
||||
|
||||
|
||||
// Validate configuration
|
||||
config.validate()?;
|
||||
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -196,8 +196,18 @@ impl AppConfig {
|
||||
tracing::info!(" Database URL: {}", mask_sensitive(&self.database.url));
|
||||
tracing::info!(" Web server: {}:{}", self.web.host, self.web.port);
|
||||
tracing::info!(" Log level: {}", self.logging.level);
|
||||
tracing::info!(" Telegram bot: {}", if self.telegram.bot_token.is_empty() { "disabled" } else { "enabled" });
|
||||
tracing::info!(" Xray config path: {}", self.xray.config_template_path.display());
|
||||
tracing::info!(
|
||||
" Telegram bot: {}",
|
||||
if self.telegram.bot_token.is_empty() {
|
||||
"disabled"
|
||||
} else {
|
||||
"enabled"
|
||||
}
|
||||
);
|
||||
tracing::info!(
|
||||
" Xray config path: {}",
|
||||
self.xray.config_template_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +226,7 @@ fn mask_sensitive(url: &str) -> String {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to URL parsing if simple approach fails
|
||||
if let Ok(parsed) = url::Url::parse(url) {
|
||||
if parsed.password().is_some() {
|
||||
@@ -249,4 +259,4 @@ mod tests {
|
||||
assert!(masked.contains("***"));
|
||||
assert!(!masked.contains("password"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
@@ -7,29 +7,29 @@ use serde::{Deserialize, Serialize};
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -180,7 +182,7 @@ 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,
|
||||
@@ -220,14 +222,14 @@ impl Model {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -246,4 +248,4 @@ impl From<CreateCertificateDto> for ActiveModel {
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -8,18 +8,18 @@ use uuid::Uuid;
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
|
||||
pub name: String,
|
||||
|
||||
|
||||
pub provider_type: String, // "cloudflare", "route53", etc.
|
||||
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub api_token: String, // Encrypted storage in production
|
||||
|
||||
|
||||
pub is_active: bool,
|
||||
|
||||
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -100,7 +102,7 @@ impl Model {
|
||||
/// Update this model with data from UpdateDnsProviderDto
|
||||
pub fn apply_update(self, dto: UpdateDnsProviderDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
|
||||
if let Some(name) = dto.name {
|
||||
active_model.name = Set(name);
|
||||
}
|
||||
@@ -110,11 +112,11 @@ impl Model {
|
||||
if let Some(is_active) = dto.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
active_model
|
||||
}
|
||||
|
||||
|
||||
/// Convert to response DTO (without exposing API token)
|
||||
pub fn to_response_dto(&self) -> DnsProviderResponseDto {
|
||||
DnsProviderResponseDto {
|
||||
@@ -142,15 +144,15 @@ impl DnsProviderType {
|
||||
DnsProviderType::Cloudflare => "cloudflare",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"cloudflare" => Some(DnsProviderType::Cloudflare),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn all() -> Vec<Self> {
|
||||
vec![DnsProviderType::Cloudflare]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -8,29 +8,29 @@ use serde_json::Value;
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -60,7 +60,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -187,9 +189,9 @@ impl From<Model> for InboundTemplateResponse {
|
||||
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!({}));
|
||||
|
||||
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),
|
||||
@@ -212,17 +214,20 @@ impl Model {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn apply_variables(&self, values: &serde_json::Map<String, Value>) -> Result<(Value, Value), String> {
|
||||
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 {
|
||||
@@ -233,18 +238,18 @@ impl Model {
|
||||
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);
|
||||
}
|
||||
@@ -267,12 +272,13 @@ impl Model {
|
||||
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![])));
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -8,24 +8,24 @@ use uuid::Uuid;
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
|
||||
/// Reference to the actual user
|
||||
pub user_id: Uuid,
|
||||
|
||||
|
||||
pub server_inbound_id: Uuid,
|
||||
|
||||
|
||||
/// Generated xray user ID (UUID for protocols like vmess/vless)
|
||||
pub xray_user_id: String,
|
||||
|
||||
|
||||
/// Generated password for protocols like trojan/shadowsocks
|
||||
pub password: Option<String>,
|
||||
|
||||
|
||||
pub level: i32,
|
||||
|
||||
|
||||
pub is_active: bool,
|
||||
|
||||
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -99,12 +101,12 @@ impl CreateInboundUserDto {
|
||||
pub fn generate_xray_user_id(&self) -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
|
||||
/// Generate random password (for trojan/shadowsocks)
|
||||
pub fn generate_password(&self) -> String {
|
||||
use rand::prelude::*;
|
||||
use rand::distributions::Alphanumeric;
|
||||
|
||||
use rand::prelude::*;
|
||||
|
||||
thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(24)
|
||||
@@ -123,7 +125,7 @@ pub struct UpdateInboundUserDto {
|
||||
impl From<CreateInboundUserDto> for ActiveModel {
|
||||
fn from(dto: CreateInboundUserDto) -> Self {
|
||||
let xray_user_id = dto.generate_xray_user_id();
|
||||
|
||||
|
||||
Self {
|
||||
user_id: Set(dto.user_id),
|
||||
server_inbound_id: Set(dto.server_inbound_id),
|
||||
@@ -140,17 +142,17 @@ 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(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
|
||||
}
|
||||
|
||||
|
||||
/// Generate email for xray client based on user information
|
||||
pub fn generate_client_email(&self, username: &str) -> String {
|
||||
format!("{}@OutFleet", username)
|
||||
@@ -185,4 +187,4 @@ impl From<Model> for InboundUserResponse {
|
||||
updated_at: model.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
pub mod user;
|
||||
pub mod certificate;
|
||||
pub mod dns_provider;
|
||||
pub mod inbound_template;
|
||||
pub mod inbound_users;
|
||||
pub mod server;
|
||||
pub mod server_inbound;
|
||||
pub mod user_access;
|
||||
pub mod inbound_users;
|
||||
pub mod telegram_config;
|
||||
pub mod user;
|
||||
pub mod user_access;
|
||||
pub mod user_request;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::user::Entity as User;
|
||||
pub use super::certificate::Entity as Certificate;
|
||||
pub use super::dns_provider::Entity as DnsProvider;
|
||||
pub use super::inbound_template::Entity as InboundTemplate;
|
||||
pub use super::inbound_users::Entity as InboundUsers;
|
||||
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;
|
||||
pub use super::telegram_config::Entity as TelegramConfig;
|
||||
pub use super::user::Entity as User;
|
||||
pub use super::user_access::Entity as UserAccess;
|
||||
pub use super::user_request::Entity as UserRequest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
@@ -7,24 +7,24 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
|
||||
pub name: String,
|
||||
|
||||
|
||||
pub hostname: String,
|
||||
|
||||
|
||||
pub grpc_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,
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -185,7 +187,7 @@ impl From<Model> for ServerResponse {
|
||||
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);
|
||||
}
|
||||
@@ -207,16 +209,23 @@ impl Model {
|
||||
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 {
|
||||
let hostname = if self.grpc_hostname.is_empty() {
|
||||
tracing::debug!("Using public hostname '{}' for gRPC (grpc_hostname is empty)", self.hostname);
|
||||
tracing::debug!(
|
||||
"Using public hostname '{}' for gRPC (grpc_hostname is empty)",
|
||||
self.hostname
|
||||
);
|
||||
&self.hostname
|
||||
} else {
|
||||
tracing::debug!("Using dedicated gRPC hostname '{}' (different from public hostname '{}')", self.grpc_hostname, self.hostname);
|
||||
tracing::debug!(
|
||||
"Using dedicated gRPC hostname '{}' (different from public hostname '{}')",
|
||||
self.grpc_hostname,
|
||||
self.hostname
|
||||
);
|
||||
&self.grpc_hostname
|
||||
};
|
||||
let endpoint = format!("{}:{}", hostname, self.grpc_port);
|
||||
@@ -228,4 +237,4 @@ impl Model {
|
||||
pub fn get_status(&self) -> ServerStatus {
|
||||
self.status.clone().into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -8,23 +8,23 @@ use serde_json::Value;
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -82,7 +82,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -95,7 +97,6 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -145,7 +146,7 @@ impl From<Model> for ServerInboundResponse {
|
||||
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
|
||||
template_name: None, // Will be filled by repository if needed
|
||||
certificate_name: None, // Will be filled by repository if needed
|
||||
}
|
||||
}
|
||||
@@ -154,7 +155,7 @@ impl From<Model> for ServerInboundResponse {
|
||||
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);
|
||||
}
|
||||
@@ -170,7 +171,7 @@ impl Model {
|
||||
if let Some(is_active) = dto.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
|
||||
active_model
|
||||
}
|
||||
|
||||
@@ -201,4 +202,4 @@ impl From<CreateServerInboundDto> for ActiveModel {
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
@@ -7,16 +7,16 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
|
||||
/// Telegram bot token (encrypted in production)
|
||||
pub bot_token: String,
|
||||
|
||||
|
||||
/// Whether the bot is active
|
||||
pub is_active: bool,
|
||||
|
||||
|
||||
/// When the config was created
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
|
||||
/// Last time config was updated
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
@@ -40,7 +40,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -52,15 +54,15 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
} else if self.id.is_not_set() {
|
||||
self.id = Set(Uuid::new_v4());
|
||||
}
|
||||
|
||||
|
||||
if self.created_at.is_not_set() {
|
||||
self.created_at = Set(chrono::Utc::now());
|
||||
}
|
||||
|
||||
|
||||
if self.updated_at.is_not_set() {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
|
||||
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
@@ -91,4 +93,4 @@ impl Model {
|
||||
updated_at: Set(self.updated_at),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
@@ -7,23 +7,23 @@ use serde::{Deserialize, Serialize};
|
||||
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>,
|
||||
|
||||
|
||||
/// Whether the user is a Telegram admin
|
||||
pub is_telegram_admin: bool,
|
||||
|
||||
|
||||
/// When the user was registered/created
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
|
||||
/// Last time user record was updated
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
@@ -48,7 +48,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -98,7 +100,7 @@ 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);
|
||||
}
|
||||
@@ -114,7 +116,7 @@ impl Model {
|
||||
if let Some(is_admin) = dto.is_telegram_admin {
|
||||
active_model.is_telegram_admin = Set(is_admin);
|
||||
}
|
||||
|
||||
|
||||
active_model
|
||||
}
|
||||
|
||||
@@ -147,9 +149,12 @@ mod tests {
|
||||
};
|
||||
|
||||
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.comment.unwrap(),
|
||||
Some("Test comment".to_string())
|
||||
);
|
||||
assert_eq!(active_model.telegram_id.unwrap(), Some(123456789));
|
||||
}
|
||||
|
||||
@@ -193,4 +198,4 @@ mod tests {
|
||||
assert!(user_with_telegram.has_telegram());
|
||||
assert!(!user_without_telegram.has_telegram());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
@@ -7,31 +7,31 @@ use serde::{Deserialize, Serialize};
|
||||
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,
|
||||
}
|
||||
@@ -46,7 +46,7 @@ pub enum Relation {
|
||||
User,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::server::Entity",
|
||||
from = "Column::ServerId",
|
||||
from = "Column::ServerId",
|
||||
to = "super::server::Column::Id"
|
||||
)]
|
||||
Server,
|
||||
@@ -90,7 +90,9 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
) -> core::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
@@ -103,7 +105,6 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// User access creation data transfer object
|
||||
@@ -143,14 +144,14 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -185,4 +186,4 @@ impl From<Model> for UserAccessResponse {
|
||||
updated_at: model.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,9 @@ impl Model {
|
||||
parts.push(last.clone());
|
||||
}
|
||||
if parts.is_empty() {
|
||||
self.telegram_username.clone().unwrap_or_else(|| format!("User {}", self.telegram_id))
|
||||
self.telegram_username
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("User {}", self.telegram_id))
|
||||
} else {
|
||||
parts.join(" ")
|
||||
}
|
||||
@@ -130,7 +132,7 @@ pub struct UpdateUserRequestDto {
|
||||
impl From<CreateUserRequestDto> for ActiveModel {
|
||||
fn from(dto: CreateUserRequestDto) -> Self {
|
||||
use sea_orm::ActiveValue::*;
|
||||
|
||||
|
||||
ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
user_id: Set(None),
|
||||
@@ -153,20 +155,20 @@ impl From<CreateUserRequestDto> for ActiveModel {
|
||||
impl Model {
|
||||
pub fn apply_update(self, dto: UpdateUserRequestDto, processed_by: Uuid) -> ActiveModel {
|
||||
use sea_orm::ActiveValue::*;
|
||||
|
||||
|
||||
let mut active: ActiveModel = self.into();
|
||||
|
||||
|
||||
if let Some(status) = dto.status {
|
||||
active.status = Set(status);
|
||||
active.processed_by_user_id = Set(Some(processed_by));
|
||||
active.processed_at = Set(Some(chrono::Utc::now().into()));
|
||||
}
|
||||
|
||||
|
||||
if let Some(response) = dto.response_message {
|
||||
active.response_message = Set(Some(response));
|
||||
}
|
||||
|
||||
|
||||
active.updated_at = Set(chrono::Utc::now().into());
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,27 +12,10 @@ impl MigrationTrait for Migration {
|
||||
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::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()
|
||||
@@ -108,12 +91,7 @@ impl MigrationTrait for Migration {
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_users_name")
|
||||
.to_owned(),
|
||||
)
|
||||
.drop_index(Index::drop().if_exists().name("idx_users_name").to_owned())
|
||||
.await?;
|
||||
|
||||
// Drop table
|
||||
@@ -132,4 +110,4 @@ enum Users {
|
||||
TelegramId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,21 +32,9 @@ impl MigrationTrait for Migration {
|
||||
.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::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()
|
||||
@@ -117,4 +105,4 @@ enum Certificates {
|
||||
AutoRenew,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,7 @@ impl MigrationTrait for Migration {
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::Description)
|
||||
.text()
|
||||
.null(),
|
||||
)
|
||||
.col(ColumnDef::new(InboundTemplates::Description).text().null())
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::Protocol)
|
||||
.string_len(50)
|
||||
@@ -152,4 +148,4 @@ enum InboundTemplates {
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,44 +11,23 @@ impl MigrationTrait for Migration {
|
||||
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::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::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::DefaultCertificateId).uuid().null())
|
||||
.col(
|
||||
ColumnDef::new(Servers::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
@@ -133,4 +112,4 @@ enum Servers {
|
||||
enum Certificates {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,8 @@ impl MigrationTrait for Migration {
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::ServerId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::TemplateId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(ServerInbounds::ServerId).uuid().not_null())
|
||||
.col(ColumnDef::new(ServerInbounds::TemplateId).uuid().not_null())
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::Tag)
|
||||
.string_len(255)
|
||||
@@ -37,11 +29,7 @@ impl MigrationTrait for Migration {
|
||||
.integer()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::CertificateId)
|
||||
.uuid()
|
||||
.null(),
|
||||
)
|
||||
.col(ColumnDef::new(ServerInbounds::CertificateId).uuid().null())
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::VariableValues)
|
||||
.json()
|
||||
@@ -192,4 +180,4 @@ enum InboundTemplates {
|
||||
enum Certificates {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,41 +17,17 @@ impl MigrationTrait for Migration {
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::UserId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::ServerId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.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::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()
|
||||
@@ -193,4 +169,4 @@ enum Servers {
|
||||
enum ServerInbounds {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,21 +22,9 @@ impl MigrationTrait for Migration {
|
||||
.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::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()
|
||||
@@ -122,4 +110,4 @@ enum InboundUsers {
|
||||
enum ServerInbounds {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,22 +36,18 @@ impl MigrationTrait for Migration {
|
||||
ColumnDef::new(InboundUsers::UserId)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.default(Expr::val("00000000-0000-0000-0000-000000000000"))
|
||||
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add password column
|
||||
// Add password column
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(InboundUsers::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(InboundUsers::Password)
|
||||
.string()
|
||||
.null()
|
||||
)
|
||||
.add_column(ColumnDef::new(InboundUsers::Password).string().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
@@ -83,7 +79,7 @@ impl MigrationTrait for Migration {
|
||||
.from(InboundUsers::Table, InboundUsers::UserId)
|
||||
.to(Users::Table, Users::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.to_owned()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -153,7 +149,7 @@ impl MigrationTrait for Migration {
|
||||
ColumnDef::new(InboundUsers::Username)
|
||||
.string()
|
||||
.not_null()
|
||||
.default("")
|
||||
.default(""),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
@@ -167,7 +163,7 @@ impl MigrationTrait for Migration {
|
||||
ColumnDef::new(InboundUsers::Email)
|
||||
.string()
|
||||
.not_null()
|
||||
.default("")
|
||||
.default(""),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
@@ -239,4 +235,4 @@ enum InboundUsers {
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ impl MigrationTrait for Migration {
|
||||
|
||||
// Update existing servers: set grpc_hostname to hostname value
|
||||
let db = manager.get_connection();
|
||||
|
||||
|
||||
// Use raw SQL to copy hostname to grpc_hostname for existing records
|
||||
// Handle both empty strings and default empty values
|
||||
db.execute_unprepared("UPDATE servers SET grpc_hostname = hostname WHERE grpc_hostname = '' OR grpc_hostname IS NULL")
|
||||
@@ -47,4 +47,4 @@ impl MigrationTrait for Migration {
|
||||
enum Servers {
|
||||
Table,
|
||||
GrpcHostname,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,7 @@ impl MigrationTrait for Migration {
|
||||
.string_len(50)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::ApiToken)
|
||||
.text()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(DnsProviders::ApiToken).text().not_null())
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::IsActive)
|
||||
.boolean()
|
||||
@@ -93,4 +89,4 @@ enum DnsProviders {
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,23 +11,29 @@ impl MigrationTrait for Migration {
|
||||
Table::create()
|
||||
.table(TelegramConfig::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(TelegramConfig::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key())
|
||||
.col(ColumnDef::new(TelegramConfig::BotToken)
|
||||
.string()
|
||||
.not_null())
|
||||
.col(ColumnDef::new(TelegramConfig::IsActive)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false))
|
||||
.col(ColumnDef::new(TelegramConfig::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null())
|
||||
.col(ColumnDef::new(TelegramConfig::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null())
|
||||
.col(
|
||||
ColumnDef::new(TelegramConfig::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(TelegramConfig::BotToken).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(TelegramConfig::IsActive)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(TelegramConfig::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(TelegramConfig::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
@@ -48,4 +54,4 @@ pub enum TelegramConfig {
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ impl MigrationTrait for Migration {
|
||||
ColumnDef::new(Users::IsTelegramAdmin)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false)
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
@@ -37,4 +37,4 @@ impl MigrationTrait for Migration {
|
||||
enum Users {
|
||||
Table,
|
||||
IsTelegramAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ impl MigrationTrait for Migration {
|
||||
.default(Expr::cust("gen_random_uuid()")),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserRequests::UserId)
|
||||
.uuid()
|
||||
.null(), // Can be null if user doesn't exist yet
|
||||
ColumnDef::new(UserRequests::UserId).uuid().null(), // Can be null if user doesn't exist yet
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserRequests::TelegramId)
|
||||
@@ -51,16 +49,8 @@ impl MigrationTrait for Migration {
|
||||
.not_null()
|
||||
.default("pending"), // pending, approved, declined
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserRequests::RequestMessage)
|
||||
.text()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserRequests::ResponseMessage)
|
||||
.text()
|
||||
.null(),
|
||||
)
|
||||
.col(ColumnDef::new(UserRequests::RequestMessage).text().null())
|
||||
.col(ColumnDef::new(UserRequests::ResponseMessage).text().null())
|
||||
.col(
|
||||
ColumnDef::new(UserRequests::ProcessedByUserId)
|
||||
.uuid()
|
||||
@@ -190,4 +180,4 @@ enum UserRequests {
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,4 @@ impl MigrationTrait for Migration {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ impl MigrationTrait for Migration {
|
||||
.add_column(
|
||||
ColumnDef::new(UserRequests::Language)
|
||||
.string()
|
||||
.default("en") // Default to English
|
||||
.default("en"), // Default to English
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
@@ -38,4 +38,4 @@ impl MigrationTrait for Migration {
|
||||
enum UserRequests {
|
||||
Table,
|
||||
Language,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20251018_000003_add_language_to_user_requests::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{Database, DatabaseConnection, ConnectOptions, Statement, DatabaseBackend, ConnectionTrait};
|
||||
use sea_orm::{
|
||||
ConnectOptions, ConnectionTrait, Database, DatabaseBackend, DatabaseConnection, Statement,
|
||||
};
|
||||
use sea_orm_migration::MigratorTrait;
|
||||
use std::time::Duration;
|
||||
use tracing::{info, warn};
|
||||
@@ -22,10 +24,10 @@ 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)
|
||||
@@ -37,16 +39,16 @@ impl DatabaseManager {
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -58,7 +60,7 @@ impl DatabaseManager {
|
||||
/// 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");
|
||||
@@ -99,21 +101,22 @@ impl DatabaseManager {
|
||||
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);
|
||||
|
||||
let encoded_url =
|
||||
format!("{}{}:{}{}", scheme, user, encoded_password, host_db);
|
||||
|
||||
return Ok(encoded_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If parsing fails, return original URL
|
||||
Ok(url.to_string())
|
||||
}
|
||||
@@ -132,7 +135,10 @@ mod tests {
|
||||
|
||||
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");
|
||||
assert_eq!(
|
||||
encoded_normal,
|
||||
"postgresql://user:password@localhost:5432/db"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -158,4 +164,4 @@ mod tests {
|
||||
assert!(health.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use sea_orm::*;
|
||||
use crate::database::entities::{certificate, prelude::*};
|
||||
use anyhow::Result;
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -13,11 +13,14 @@ impl CertificateRepository {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn create(&self, cert_data: certificate::CreateCertificateDto) -> Result<certificate::Model> {
|
||||
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?
|
||||
@@ -48,7 +51,11 @@ impl CertificateRepository {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, cert_data: certificate::UpdateCertificateDto) -> Result<certificate::Model> {
|
||||
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?
|
||||
@@ -66,7 +73,7 @@ impl CertificateRepository {
|
||||
|
||||
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)
|
||||
@@ -75,11 +82,11 @@ impl CertificateRepository {
|
||||
|
||||
/// Update certificate data (cert and key) and expiration date
|
||||
pub async fn update_certificate_data(
|
||||
&self,
|
||||
id: Uuid,
|
||||
cert_pem: &str,
|
||||
&self,
|
||||
id: Uuid,
|
||||
cert_pem: &str,
|
||||
key_pem: &str,
|
||||
expires_at: chrono::DateTime<chrono::Utc>
|
||||
expires_at: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<certificate::Model> {
|
||||
let mut cert: certificate::ActiveModel = Certificate::find_by_id(id)
|
||||
.one(&self.db)
|
||||
@@ -94,4 +101,4 @@ impl CertificateRepository {
|
||||
|
||||
Ok(cert.update(&self.db).await?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter, Set, PaginatorTrait};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
Set,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::dns_provider::{
|
||||
Entity, Model, ActiveModel, CreateDnsProviderDto, UpdateDnsProviderDto, Column, DnsProviderType
|
||||
ActiveModel, Column, CreateDnsProviderDto, DnsProviderType, Entity, Model, UpdateDnsProviderDto,
|
||||
};
|
||||
|
||||
pub struct DnsProviderRepository {
|
||||
@@ -89,7 +92,7 @@ impl DnsProviderRepository {
|
||||
let mut active_model: ActiveModel = provider.into();
|
||||
active_model.is_active = Set(true);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
|
||||
let updated_provider = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated_provider))
|
||||
}
|
||||
@@ -103,7 +106,7 @@ impl DnsProviderRepository {
|
||||
let mut active_model: ActiveModel = provider.into();
|
||||
active_model.is_active = Set(false);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
|
||||
let updated_provider = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated_provider))
|
||||
}
|
||||
@@ -111,17 +114,20 @@ impl DnsProviderRepository {
|
||||
/// Check if a provider name already exists
|
||||
pub async fn name_exists(&self, name: &str, exclude_id: Option<Uuid>) -> Result<bool> {
|
||||
let mut query = Entity::find().filter(Column::Name.eq(name));
|
||||
|
||||
|
||||
if let Some(id) = exclude_id {
|
||||
query = query.filter(Column::Id.ne(id));
|
||||
}
|
||||
|
||||
|
||||
let count = query.count(&self.db).await?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Get the first active provider of a specific type
|
||||
pub async fn get_active_provider_by_type(&self, provider_type: DnsProviderType) -> Result<Option<Model>> {
|
||||
pub async fn get_active_provider_by_type(
|
||||
&self,
|
||||
provider_type: DnsProviderType,
|
||||
) -> Result<Option<Model>> {
|
||||
let provider = Entity::find()
|
||||
.filter(Column::ProviderType.eq(provider_type.as_str()))
|
||||
.filter(Column::IsActive.eq(true))
|
||||
@@ -129,4 +135,4 @@ impl DnsProviderRepository {
|
||||
.await?;
|
||||
Ok(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use sea_orm::*;
|
||||
use crate::database::entities::{inbound_template, prelude::*};
|
||||
use anyhow::Result;
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -14,11 +14,14 @@ impl InboundTemplateRepository {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn create(&self, template_data: inbound_template::CreateInboundTemplateDto) -> Result<inbound_template::Model> {
|
||||
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?
|
||||
@@ -47,7 +50,11 @@ impl InboundTemplateRepository {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, template_data: inbound_template::UpdateInboundTemplateDto) -> Result<inbound_template::Model> {
|
||||
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?
|
||||
@@ -62,4 +69,4 @@ impl InboundTemplateRepository {
|
||||
let result = InboundTemplate::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter, Set};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::inbound_users::{
|
||||
Entity, Model, ActiveModel, CreateInboundUserDto, UpdateInboundUserDto, Column
|
||||
ActiveModel, Column, CreateInboundUserDto, Entity, Model, UpdateInboundUserDto,
|
||||
};
|
||||
use crate::services::uri_generator::ClientConfigData;
|
||||
|
||||
@@ -46,7 +46,11 @@ impl InboundUsersRepository {
|
||||
}
|
||||
|
||||
/// Find user by user_id and inbound (for uniqueness check - one user per inbound)
|
||||
pub async fn find_by_user_and_inbound(&self, user_id: Uuid, inbound_id: Uuid) -> Result<Option<Model>> {
|
||||
pub async fn find_by_user_and_inbound(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
inbound_id: Uuid,
|
||||
) -> Result<Option<Model>> {
|
||||
let user = Entity::find()
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
.filter(Column::ServerInboundId.eq(inbound_id))
|
||||
@@ -96,7 +100,7 @@ impl InboundUsersRepository {
|
||||
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))
|
||||
}
|
||||
@@ -111,7 +115,7 @@ impl InboundUsersRepository {
|
||||
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))
|
||||
}
|
||||
@@ -126,17 +130,25 @@ impl InboundUsersRepository {
|
||||
}
|
||||
|
||||
/// Check if user already has access to this inbound
|
||||
pub async fn user_has_access_to_inbound(&self, user_id: Uuid, inbound_id: Uuid) -> Result<bool> {
|
||||
pub async fn user_has_access_to_inbound(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
inbound_id: Uuid,
|
||||
) -> Result<bool> {
|
||||
let exists = self.find_by_user_and_inbound(user_id, inbound_id).await?;
|
||||
Ok(exists.is_some())
|
||||
}
|
||||
|
||||
/// Get complete client configuration data for URI generation
|
||||
pub async fn get_client_config_data(&self, user_id: Uuid, server_inbound_id: Uuid) -> Result<Option<ClientConfigData>> {
|
||||
pub async fn get_client_config_data(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
server_inbound_id: Uuid,
|
||||
) -> Result<Option<ClientConfigData>> {
|
||||
use crate::database::entities::{
|
||||
user, server, server_inbound, inbound_template, certificate
|
||||
certificate, inbound_template, server, server_inbound, user,
|
||||
};
|
||||
|
||||
|
||||
// Get the inbound_user record first
|
||||
let inbound_user = Entity::find()
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
@@ -144,32 +156,34 @@ impl InboundUsersRepository {
|
||||
.filter(Column::IsActive.eq(true))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
|
||||
if let Some(inbound_user) = inbound_user {
|
||||
// Get user info
|
||||
let user_entity = user::Entity::find_by_id(inbound_user.user_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("User not found"))?;
|
||||
|
||||
|
||||
// Get server inbound info
|
||||
let server_inbound_entity = server_inbound::Entity::find_by_id(inbound_user.server_inbound_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
|
||||
|
||||
let server_inbound_entity =
|
||||
server_inbound::Entity::find_by_id(inbound_user.server_inbound_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
|
||||
|
||||
// Get server info
|
||||
let server_entity = server::Entity::find_by_id(server_inbound_entity.server_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server not found"))?;
|
||||
|
||||
|
||||
// Get template info
|
||||
let template_entity = inbound_template::Entity::find_by_id(server_inbound_entity.template_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Template not found"))?;
|
||||
|
||||
let template_entity =
|
||||
inbound_template::Entity::find_by_id(server_inbound_entity.template_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Template not found"))?;
|
||||
|
||||
// Get certificate info (optional)
|
||||
let certificate_domain = if let Some(cert_id) = server_inbound_entity.certificate_id {
|
||||
certificate::Entity::find_by_id(cert_id)
|
||||
@@ -179,14 +193,16 @@ impl InboundUsersRepository {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
|
||||
let config = ClientConfigData {
|
||||
user_name: user_entity.name,
|
||||
xray_user_id: inbound_user.xray_user_id,
|
||||
password: inbound_user.password,
|
||||
level: inbound_user.level,
|
||||
hostname: server_entity.hostname,
|
||||
port: server_inbound_entity.port_override.unwrap_or(template_entity.default_port),
|
||||
port: server_inbound_entity
|
||||
.port_override
|
||||
.unwrap_or(template_entity.default_port),
|
||||
protocol: template_entity.protocol,
|
||||
stream_settings: template_entity.stream_settings,
|
||||
base_settings: template_entity.base_settings,
|
||||
@@ -197,7 +213,7 @@ impl InboundUsersRepository {
|
||||
inbound_tag: server_inbound_entity.tag,
|
||||
template_name: template_entity.name,
|
||||
};
|
||||
|
||||
|
||||
Ok(Some(config))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -205,23 +221,29 @@ impl InboundUsersRepository {
|
||||
}
|
||||
|
||||
/// Get all client configuration data for a user
|
||||
pub async fn get_all_client_configs_for_user(&self, user_id: Uuid) -> Result<Vec<ClientConfigData>> {
|
||||
pub async fn get_all_client_configs_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<ClientConfigData>> {
|
||||
// Get all active inbound users for this user
|
||||
let inbound_users = Entity::find()
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
.filter(Column::IsActive.eq(true))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
|
||||
let mut configs = Vec::new();
|
||||
|
||||
|
||||
for inbound_user in inbound_users {
|
||||
// Get the client config data for each inbound
|
||||
if let Ok(Some(config)) = self.get_client_config_data(user_id, inbound_user.server_inbound_id).await {
|
||||
if let Ok(Some(config)) = self
|
||||
.get_client_config_data(user_id, inbound_user.server_inbound_id)
|
||||
.await
|
||||
{
|
||||
configs.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(configs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
pub mod user;
|
||||
pub mod certificate;
|
||||
pub mod dns_provider;
|
||||
pub mod inbound_template;
|
||||
pub mod inbound_users;
|
||||
pub mod server;
|
||||
pub mod server_inbound;
|
||||
pub mod user_access;
|
||||
pub mod inbound_users;
|
||||
pub mod telegram_config;
|
||||
pub mod user;
|
||||
pub mod user_access;
|
||||
pub mod user_request;
|
||||
|
||||
pub use user::UserRepository;
|
||||
pub use certificate::CertificateRepository;
|
||||
pub use dns_provider::DnsProviderRepository;
|
||||
pub use inbound_template::InboundTemplateRepository;
|
||||
pub use inbound_users::InboundUsersRepository;
|
||||
pub use server::ServerRepository;
|
||||
pub use server_inbound::ServerInboundRepository;
|
||||
pub use user_access::UserAccessRepository;
|
||||
pub use inbound_users::InboundUsersRepository;
|
||||
pub use telegram_config::TelegramConfigRepository;
|
||||
pub use user_request::UserRequestRepository;
|
||||
pub use user::UserRepository;
|
||||
pub use user_access::UserAccessRepository;
|
||||
pub use user_request::UserRequestRepository;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use sea_orm::*;
|
||||
use crate::database::entities::{server, prelude::*};
|
||||
use crate::database::entities::{prelude::*, server};
|
||||
use anyhow::Result;
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -18,7 +18,7 @@ impl ServerRepository {
|
||||
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?
|
||||
@@ -54,7 +54,11 @@ impl ServerRepository {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, server_data: server::UpdateServerDto) -> Result<server::Model> {
|
||||
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?
|
||||
@@ -71,9 +75,11 @@ impl ServerRepository {
|
||||
}
|
||||
|
||||
pub async fn get_grpc_endpoint(&self, id: Uuid) -> Result<String> {
|
||||
let server = self.find_by_id(id).await?
|
||||
let server = self
|
||||
.find_by_id(id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server not found"))?;
|
||||
|
||||
|
||||
Ok(server.get_grpc_endpoint())
|
||||
}
|
||||
|
||||
@@ -85,4 +91,4 @@ impl ServerRepository {
|
||||
let count = Server::find().count(&self.db).await?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use sea_orm::*;
|
||||
use crate::database::entities::{server_inbound, prelude::*};
|
||||
use crate::database::entities::{prelude::*, server_inbound};
|
||||
use anyhow::Result;
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -14,7 +14,11 @@ impl ServerInboundRepository {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn create(&self, server_id: Uuid, inbound_data: server_inbound::CreateServerInboundDto) -> Result<server_inbound::Model> {
|
||||
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);
|
||||
@@ -22,26 +26,31 @@ impl ServerInboundRepository {
|
||||
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> {
|
||||
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?
|
||||
@@ -63,9 +72,12 @@ impl ServerInboundRepository {
|
||||
.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};
|
||||
|
||||
pub async fn find_by_server_id_with_template(
|
||||
&self,
|
||||
server_id: Uuid,
|
||||
) -> Result<Vec<server_inbound::ServerInboundResponse>> {
|
||||
use crate::database::entities::{certificate, inbound_template};
|
||||
|
||||
let inbounds = ServerInbound::find()
|
||||
.filter(server_inbound::Column::ServerId.eq(server_id))
|
||||
.all(&self.db)
|
||||
@@ -74,26 +86,33 @@ impl ServerInboundRepository {
|
||||
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 {
|
||||
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 {
|
||||
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>> {
|
||||
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)
|
||||
@@ -107,14 +126,20 @@ impl ServerInboundRepository {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_certificate_id(&self, certificate_id: Uuid) -> Result<Vec<server_inbound::Model>> {
|
||||
pub async fn find_by_certificate_id(
|
||||
&self,
|
||||
certificate_id: Uuid,
|
||||
) -> Result<Vec<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find()
|
||||
.filter(server_inbound::Column::CertificateId.eq(certificate_id))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_active_by_server(&self, server_id: Uuid) -> Result<Vec<server_inbound::Model>> {
|
||||
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))
|
||||
@@ -122,7 +147,11 @@ impl ServerInboundRepository {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, inbound_data: server_inbound::UpdateServerInboundDto) -> Result<server_inbound::Model> {
|
||||
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?
|
||||
@@ -175,4 +204,4 @@ impl ServerInboundRepository {
|
||||
let count = ServerInbound::find().count(&self.db).await?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait, ActiveModelTrait, Set, QueryFilter, ColumnTrait, QueryOrder};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, Set,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::telegram_config::{
|
||||
self, Model, CreateTelegramConfigDto, UpdateTelegramConfigDto
|
||||
self, CreateTelegramConfigDto, Model, UpdateTelegramConfigDto,
|
||||
};
|
||||
|
||||
pub struct TelegramConfigRepository {
|
||||
@@ -88,7 +90,7 @@ impl TelegramConfigRepository {
|
||||
/// Activate a configuration (deactivates all others)
|
||||
pub async fn activate(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
self.deactivate_all_except(id).await?;
|
||||
|
||||
|
||||
let model = telegram_config::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
@@ -164,4 +166,4 @@ impl TelegramConfigRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait, QuerySelect};
|
||||
use sea_orm::{
|
||||
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
|
||||
QuerySelect,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use crate::database::entities::user::{Entity as User, Column, Model, ActiveModel, CreateUserDto, UpdateUserDto};
|
||||
use crate::database::entities::user::{
|
||||
ActiveModel, Column, CreateUserDto, Entity as User, Model, UpdateUserDto,
|
||||
};
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
|
||||
pub struct UserRepository {
|
||||
db: DatabaseConnection,
|
||||
@@ -46,7 +51,12 @@ impl UserRepository {
|
||||
}
|
||||
|
||||
/// Search users by name (with pagination for backward compatibility)
|
||||
pub async fn search_by_name(&self, query: &str, page: u64, per_page: u64) -> Result<Vec<Model>> {
|
||||
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)
|
||||
@@ -60,22 +70,22 @@ impl UserRepository {
|
||||
/// Universal search - searches by name, telegram_id, or user_id
|
||||
pub async fn search(&self, query: &str) -> Result<Vec<Model>> {
|
||||
use sea_orm::Condition;
|
||||
|
||||
|
||||
let mut condition = Condition::any();
|
||||
|
||||
|
||||
// Search by name (case-insensitive partial match)
|
||||
condition = condition.add(Column::Name.contains(query));
|
||||
|
||||
|
||||
// Try to parse as telegram_id (i64)
|
||||
if let Ok(telegram_id) = query.parse::<i64>() {
|
||||
condition = condition.add(Column::TelegramId.eq(telegram_id));
|
||||
}
|
||||
|
||||
|
||||
// Try to parse as UUID (user_id)
|
||||
if let Ok(user_id) = Uuid::parse_str(query) {
|
||||
condition = condition.add(Column::Id.eq(user_id));
|
||||
}
|
||||
|
||||
|
||||
let users = User::find()
|
||||
.filter(condition)
|
||||
.order_by_desc(Column::CreatedAt)
|
||||
@@ -89,7 +99,9 @@ impl UserRepository {
|
||||
/// 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?;
|
||||
let user = User::insert(active_model)
|
||||
.exec_with_returning(&self.db)
|
||||
.await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
@@ -126,14 +138,13 @@ impl UserRepository {
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
|
||||
/// Set user as Telegram admin
|
||||
pub async fn set_telegram_admin(&self, user_id: Uuid, is_admin: bool) -> Result<Option<Model>> {
|
||||
if let Some(user) = self.get_by_id(user_id).await? {
|
||||
let mut active_model: ActiveModel = user.into();
|
||||
active_model.is_telegram_admin = Set(is_admin);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
|
||||
let updated = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated))
|
||||
} else {
|
||||
@@ -168,29 +179,46 @@ impl UserRepository {
|
||||
.await?;
|
||||
Ok(admins)
|
||||
}
|
||||
|
||||
|
||||
/// Get the first admin user (for system operations)
|
||||
pub async fn get_first_admin(&self) -> Result<Option<Model>> {
|
||||
let admin = User::find()
|
||||
.filter(Column::IsTelegramAdmin.eq(true))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
|
||||
Ok(admin)
|
||||
}
|
||||
|
||||
/// Count total users
|
||||
pub async fn count_all(&self) -> Result<i64> {
|
||||
let count = User::find().count(&self.db).await?;
|
||||
|
||||
Ok(count as i64)
|
||||
}
|
||||
|
||||
/// Find users with pagination
|
||||
pub async fn find_paginated(&self, offset: u64, limit: u64) -> Result<Vec<Model>> {
|
||||
let users = User::find()
|
||||
.order_by_desc(Column::CreatedAt)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::config::DatabaseConfig;
|
||||
use crate::database::DatabaseManager;
|
||||
|
||||
async fn setup_test_db() -> Result<UserRepository> {
|
||||
let config = DatabaseConfig {
|
||||
url: std::env::var("DATABASE_URL").unwrap_or_else(|_|
|
||||
"sqlite::memory:".to_string()
|
||||
),
|
||||
url: std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite::memory:".to_string()),
|
||||
max_connections: 5,
|
||||
connection_timeout: 30,
|
||||
auto_migrate: true,
|
||||
@@ -243,4 +271,4 @@ mod tests {
|
||||
let deleted_user = repo.get_by_id(created_user.id).await.unwrap();
|
||||
assert!(deleted_user.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::database::entities::user_access::{self, Entity as UserAccess, Model, ActiveModel, CreateUserAccessDto, UpdateUserAccessDto};
|
||||
use crate::database::entities::user_access::{
|
||||
self, ActiveModel, CreateUserAccessDto, Entity as UserAccess, Model, UpdateUserAccessDto,
|
||||
};
|
||||
|
||||
pub struct UserAccessRepository {
|
||||
db: DatabaseConnection,
|
||||
@@ -35,7 +37,11 @@ impl UserAccessRepository {
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
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))
|
||||
@@ -45,7 +51,12 @@ impl UserAccessRepository {
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
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))
|
||||
@@ -83,18 +94,26 @@ impl UserAccessRepository {
|
||||
|
||||
/// 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
|
||||
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
|
||||
self.update(
|
||||
id,
|
||||
UpdateUserAccessDto {
|
||||
is_active: Some(false),
|
||||
level: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get all active access for a user
|
||||
@@ -115,4 +134,4 @@ impl UserAccessRepository {
|
||||
.await?;
|
||||
Ok(result.rows_affected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, DatabaseConnection, QueryOrder, PaginatorTrait, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
use crate::database::entities::user_request::{
|
||||
self, Model, ActiveModel, CreateUserRequestDto, UpdateUserRequestDto, RequestStatus
|
||||
self, ActiveModel, CreateUserRequestDto, Model, RequestStatus, UpdateUserRequestDto,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use sea_orm::{
|
||||
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
|
||||
QuerySelect,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct UserRequestRepository {
|
||||
db: DatabaseConnection,
|
||||
@@ -18,10 +21,10 @@ impl UserRequestRepository {
|
||||
let paginator = user_request::Entity::find()
|
||||
.order_by_desc(user_request::Column::CreatedAt)
|
||||
.paginate(&self.db, per_page);
|
||||
|
||||
|
||||
let total = paginator.num_items().await?;
|
||||
let items = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
|
||||
Ok((items, total))
|
||||
}
|
||||
|
||||
@@ -30,17 +33,15 @@ impl UserRequestRepository {
|
||||
.filter(user_request::Column::Status.eq("pending"))
|
||||
.order_by_desc(user_request::Column::CreatedAt)
|
||||
.paginate(&self.db, per_page);
|
||||
|
||||
|
||||
let total = paginator.num_items().await?;
|
||||
let items = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
|
||||
Ok((items, total))
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let request = user_request::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
let request = user_request::Entity::find_by_id(id).one(&self.db).await?;
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
@@ -73,6 +74,25 @@ impl UserRequestRepository {
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
/// Count total requests
|
||||
pub async fn count_all(&self) -> Result<i64> {
|
||||
let count = user_request::Entity::find().count(&self.db).await?;
|
||||
|
||||
Ok(count as i64)
|
||||
}
|
||||
|
||||
/// Find requests with pagination
|
||||
pub async fn find_paginated(&self, offset: u64, limit: u64) -> Result<Vec<Model>> {
|
||||
let requests = user_request::Entity::find()
|
||||
.order_by_desc(user_request::Column::CreatedAt)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
Ok(requests)
|
||||
}
|
||||
|
||||
pub async fn create(&self, dto: CreateUserRequestDto) -> Result<Model> {
|
||||
use sea_orm::ActiveModelTrait;
|
||||
let active_model: ActiveModel = dto.into();
|
||||
@@ -80,11 +100,14 @@ impl UserRequestRepository {
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, dto: UpdateUserRequestDto, processed_by: Uuid) -> Result<Option<Model>> {
|
||||
let model = user_request::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: UpdateUserRequestDto,
|
||||
processed_by: Uuid,
|
||||
) -> Result<Option<Model>> {
|
||||
let model = user_request::Entity::find_by_id(id).one(&self.db).await?;
|
||||
|
||||
match model {
|
||||
Some(model) => {
|
||||
use sea_orm::ActiveModelTrait;
|
||||
@@ -96,7 +119,12 @@ impl UserRequestRepository {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn approve(&self, id: Uuid, response_message: Option<String>, processed_by: Uuid) -> Result<Option<Model>> {
|
||||
pub async fn approve(
|
||||
&self,
|
||||
id: Uuid,
|
||||
response_message: Option<String>,
|
||||
processed_by: Uuid,
|
||||
) -> Result<Option<Model>> {
|
||||
let dto = UpdateUserRequestDto {
|
||||
status: Some(RequestStatus::Approved.as_str().to_string()),
|
||||
response_message,
|
||||
@@ -105,7 +133,12 @@ impl UserRequestRepository {
|
||||
self.update(id, dto, processed_by).await
|
||||
}
|
||||
|
||||
pub async fn decline(&self, id: Uuid, response_message: Option<String>, processed_by: Uuid) -> Result<Option<Model>> {
|
||||
pub async fn decline(
|
||||
&self,
|
||||
id: Uuid,
|
||||
response_message: Option<String>,
|
||||
processed_by: Uuid,
|
||||
) -> Result<Option<Model>> {
|
||||
let dto = UpdateUserRequestDto {
|
||||
status: Some(RequestStatus::Declined.as_str().to_string()),
|
||||
response_message,
|
||||
@@ -128,4 +161,4 @@ impl UserRequestRepository {
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
src/main.rs
28
src/main.rs
@@ -7,9 +7,9 @@ mod database;
|
||||
mod services;
|
||||
mod web;
|
||||
|
||||
use config::{AppConfig, args::parse_args};
|
||||
use config::{args::parse_args, AppConfig};
|
||||
use database::DatabaseManager;
|
||||
use services::{TaskScheduler, XrayService, TelegramService};
|
||||
use services::{TaskScheduler, TelegramService, XrayService};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@@ -24,7 +24,6 @@ async fn main() -> Result<()> {
|
||||
// Initialize logging early with basic configuration
|
||||
init_logging(&args.log_level.as_deref().unwrap_or("info"))?;
|
||||
|
||||
|
||||
// Handle special flags
|
||||
if args.print_default_config {
|
||||
print_default_config()?;
|
||||
@@ -33,9 +32,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
// Load configuration
|
||||
let config = match AppConfig::load() {
|
||||
Ok(config) => {
|
||||
config
|
||||
}
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to load configuration: {}", e);
|
||||
if args.validate_config {
|
||||
@@ -58,12 +55,9 @@ async fn main() -> Result<()> {
|
||||
config::env::EnvVars::print_env_info();
|
||||
}
|
||||
|
||||
|
||||
// Initialize database connection
|
||||
let db = match DatabaseManager::new(&config.database).await {
|
||||
Ok(db) => {
|
||||
db
|
||||
}
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to initialize database: {}", e);
|
||||
return Err(e);
|
||||
@@ -82,7 +76,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
// Initialize xray service
|
||||
let xray_service = XrayService::new();
|
||||
|
||||
|
||||
// Initialize and start task scheduler with dependencies
|
||||
let mut task_scheduler = TaskScheduler::new().await?;
|
||||
task_scheduler.start(db.clone(), xray_service).await?;
|
||||
@@ -97,7 +91,7 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// Start web server with task scheduler
|
||||
|
||||
|
||||
tokio::select! {
|
||||
result = web::start_server(db, config.clone(), Some(telegram_service.clone())) => {
|
||||
match result {
|
||||
@@ -123,12 +117,12 @@ fn init_logging(level: &str) -> Result<()> {
|
||||
.with(filter)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_target(true) // Show module names
|
||||
.with_target(true) // Show module names
|
||||
.with_thread_ids(false)
|
||||
.with_thread_names(false)
|
||||
.with_file(false)
|
||||
.with_line_number(false)
|
||||
.compact()
|
||||
.compact(),
|
||||
)
|
||||
.try_init()?;
|
||||
|
||||
@@ -138,11 +132,11 @@ fn init_logging(level: &str) -> Result<()> {
|
||||
fn print_default_config() -> Result<()> {
|
||||
let default_config = AppConfig::default();
|
||||
let toml_content = toml::to_string_pretty(&default_config)?;
|
||||
|
||||
|
||||
println!("# Default configuration for Xray Admin Panel");
|
||||
println!("# Save this to config.toml and modify as needed\n");
|
||||
println!("{}", toml_content);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -179,4 +173,4 @@ mod tests {
|
||||
let masked = mask_url(url);
|
||||
assert_eq!(masked, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::services::acme::{CloudflareClient, AcmeError};
|
||||
use crate::services::acme::{AcmeError, CloudflareClient};
|
||||
|
||||
pub struct AcmeClient {
|
||||
cloudflare: CloudflareClient,
|
||||
@@ -21,7 +21,7 @@ impl AcmeClient {
|
||||
directory_url: String,
|
||||
) -> Result<Self, AcmeError> {
|
||||
info!("Creating ACME client for directory: {}", directory_url);
|
||||
|
||||
|
||||
let cloudflare = CloudflareClient::new(cloudflare_token)?;
|
||||
|
||||
// Create Let's Encrypt account
|
||||
@@ -47,17 +47,24 @@ impl AcmeClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_certificate(&mut self, domain: &str, base_domain: &str) -> Result<(String, String), AcmeError> {
|
||||
pub async fn get_certificate(
|
||||
&mut self,
|
||||
domain: &str,
|
||||
base_domain: &str,
|
||||
) -> Result<(String, String), AcmeError> {
|
||||
info!("Starting certificate request for domain: {}", domain);
|
||||
|
||||
// Validate domain
|
||||
if domain.is_empty() || base_domain.is_empty() {
|
||||
return Err(AcmeError::InvalidDomain("Domain cannot be empty".to_string()));
|
||||
return Err(AcmeError::InvalidDomain(
|
||||
"Domain cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Create a new order
|
||||
let identifiers = vec![Identifier::Dns(domain.to_string())];
|
||||
let mut order = self.account
|
||||
let mut order = self
|
||||
.account
|
||||
.new_order(&NewOrder::new(&identifiers))
|
||||
.await
|
||||
.map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
|
||||
@@ -66,13 +73,12 @@ impl AcmeClient {
|
||||
|
||||
// Process authorizations
|
||||
let mut authorizations = order.authorizations();
|
||||
|
||||
|
||||
while let Some(authz_result) = authorizations.next().await {
|
||||
let mut authz = authz_result
|
||||
.map_err(|e| AcmeError::Challenge(e.to_string()))?;
|
||||
|
||||
let mut authz = authz_result.map_err(|e| AcmeError::Challenge(e.to_string()))?;
|
||||
|
||||
let identifier = format!("{:?}", authz.identifier());
|
||||
|
||||
|
||||
if authz.status == AuthorizationStatus::Valid {
|
||||
info!("Authorization already valid for: {:?}", identifier);
|
||||
continue;
|
||||
@@ -93,7 +99,8 @@ impl AcmeClient {
|
||||
|
||||
// Create DNS record
|
||||
let challenge_domain = format!("_acme-challenge.{}", domain);
|
||||
let record_id = self.cloudflare
|
||||
let record_id = self
|
||||
.cloudflare
|
||||
.create_txt_record(base_domain, &challenge_domain, &challenge_value)
|
||||
.await?;
|
||||
|
||||
@@ -105,9 +112,11 @@ impl AcmeClient {
|
||||
|
||||
// Submit challenge
|
||||
info!("Submitting challenge...");
|
||||
challenge.set_ready().await
|
||||
challenge
|
||||
.set_ready()
|
||||
.await
|
||||
.map_err(|e| AcmeError::Challenge(e.to_string()))?;
|
||||
|
||||
|
||||
(challenge_value, record_id)
|
||||
};
|
||||
|
||||
@@ -129,7 +138,9 @@ impl AcmeClient {
|
||||
return Err(AcmeError::Challenge("Order processing timeout".to_string()));
|
||||
}
|
||||
|
||||
order.refresh().await
|
||||
order
|
||||
.refresh()
|
||||
.await
|
||||
.map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
|
||||
|
||||
match order.state().status {
|
||||
@@ -154,55 +165,73 @@ impl AcmeClient {
|
||||
// Generate CSR
|
||||
info!("Generating certificate signing request...");
|
||||
let mut params = CertificateParams::new(vec![domain.to_string()]);
|
||||
|
||||
|
||||
params.distinguished_name = DistinguishedName::new();
|
||||
|
||||
|
||||
let key_pair = KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256)
|
||||
.map_err(|e| AcmeError::CertificateGeneration(e.to_string()))?;
|
||||
|
||||
|
||||
// Set the key pair for CSR generation
|
||||
params.key_pair = Some(key_pair);
|
||||
|
||||
|
||||
// Generate CSR using rcgen certificate
|
||||
let cert = rcgen::Certificate::from_params(params)
|
||||
.map_err(|e| AcmeError::CertificateGeneration(e.to_string()))?;
|
||||
let csr_der = cert.serialize_request_der()
|
||||
let csr_der = cert
|
||||
.serialize_request_der()
|
||||
.map_err(|e| AcmeError::CertificateGeneration(e.to_string()))?;
|
||||
|
||||
// Finalize order with CSR
|
||||
info!("Finalizing order with CSR...");
|
||||
order.finalize_csr(&csr_der).await
|
||||
order
|
||||
.finalize_csr(&csr_der)
|
||||
.await
|
||||
.map_err(|e| AcmeError::CertificateGeneration(e.to_string()))?;
|
||||
|
||||
|
||||
// Wait for certificate to be ready
|
||||
info!("Waiting for certificate to be generated...");
|
||||
let start = Instant::now();
|
||||
let timeout = Duration::from_secs(300); // 5 minutes
|
||||
|
||||
|
||||
let cert_chain_pem = loop {
|
||||
if start.elapsed() > timeout {
|
||||
return Err(AcmeError::CertificateGeneration("Certificate generation timeout".to_string()));
|
||||
return Err(AcmeError::CertificateGeneration(
|
||||
"Certificate generation timeout".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
order.refresh().await
|
||||
order
|
||||
.refresh()
|
||||
.await
|
||||
.map_err(|e| AcmeError::CertificateGeneration(e.to_string()))?;
|
||||
|
||||
match order.state().status {
|
||||
OrderStatus::Valid => {
|
||||
info!("Certificate is ready!");
|
||||
break order.certificate().await
|
||||
break order
|
||||
.certificate()
|
||||
.await
|
||||
.map_err(|e| AcmeError::CertificateGeneration(e.to_string()))?
|
||||
.ok_or_else(|| AcmeError::CertificateGeneration("Certificate not available".to_string()))?;
|
||||
.ok_or_else(|| {
|
||||
AcmeError::CertificateGeneration(
|
||||
"Certificate not available".to_string(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
OrderStatus::Invalid => {
|
||||
return Err(AcmeError::CertificateGeneration("Order became invalid during certificate generation".to_string()));
|
||||
return Err(AcmeError::CertificateGeneration(
|
||||
"Order became invalid during certificate generation".to_string(),
|
||||
));
|
||||
}
|
||||
OrderStatus::Processing => {
|
||||
debug!("Certificate still being processed, waiting...");
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
_ => {
|
||||
debug!("Waiting for certificate, order status: {:?}", order.state().status);
|
||||
debug!(
|
||||
"Waiting for certificate, order status: {:?}",
|
||||
order.state().status
|
||||
);
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
}
|
||||
@@ -214,12 +243,16 @@ impl AcmeClient {
|
||||
Ok((cert_chain_pem, private_key_pem))
|
||||
}
|
||||
|
||||
async fn wait_for_dns_propagation(&self, record_name: &str, expected_value: &str) -> Result<(), AcmeError> {
|
||||
async fn wait_for_dns_propagation(
|
||||
&self,
|
||||
record_name: &str,
|
||||
expected_value: &str,
|
||||
) -> Result<(), AcmeError> {
|
||||
info!("Checking DNS propagation for: {}", record_name);
|
||||
|
||||
|
||||
let start = Instant::now();
|
||||
let timeout = Duration::from_secs(120); // 2 minutes
|
||||
|
||||
|
||||
while start.elapsed() < timeout {
|
||||
match self.check_dns_txt_record(record_name, expected_value).await {
|
||||
Ok(true) => {
|
||||
@@ -233,17 +266,21 @@ impl AcmeClient {
|
||||
debug!("DNS check failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
|
||||
|
||||
warn!("DNS propagation timeout, but continuing anyway");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_dns_txt_record(&self, record_name: &str, expected_value: &str) -> Result<bool, AcmeError> {
|
||||
async fn check_dns_txt_record(
|
||||
&self,
|
||||
record_name: &str,
|
||||
expected_value: &str,
|
||||
) -> Result<bool, AcmeError> {
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
let output = Command::new("dig")
|
||||
.args(&["+short", "TXT", record_name])
|
||||
.output()
|
||||
@@ -268,7 +305,11 @@ impl AcmeClient {
|
||||
}
|
||||
|
||||
async fn cleanup_dns_record(&self, base_domain: &str, record_id: &str) {
|
||||
if let Err(e) = self.cloudflare.delete_txt_record(base_domain, record_id).await {
|
||||
if let Err(e) = self
|
||||
.cloudflare
|
||||
.delete_txt_record(base_domain, record_id)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to cleanup DNS record {}: {:?}", record_id, e);
|
||||
}
|
||||
}
|
||||
@@ -277,11 +318,13 @@ impl AcmeClient {
|
||||
pub fn get_base_domain(domain: &str) -> Result<String, AcmeError> {
|
||||
let parts: Vec<&str> = domain.split('.').collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(AcmeError::InvalidDomain("Domain must have at least 2 parts".to_string()));
|
||||
return Err(AcmeError::InvalidDomain(
|
||||
"Domain must have at least 2 parts".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Take the last two parts for base domain
|
||||
let base_domain = format!("{}.{}", parts[parts.len() - 2], parts[parts.len() - 1]);
|
||||
Ok(base_domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +74,11 @@ impl CloudflareClient {
|
||||
|
||||
async fn get_zone_id(&self, domain: &str) -> Result<String, AcmeError> {
|
||||
info!("Getting Cloudflare zone ID for domain: {}", domain);
|
||||
|
||||
|
||||
let url = format!("https://api.cloudflare.com/client/v4/zones?name={}", domain);
|
||||
|
||||
let response = self.client
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_token))
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -87,7 +88,10 @@ impl CloudflareClient {
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(AcmeError::CloudflareApi(format!("HTTP {}: {}", status, body)));
|
||||
return Err(AcmeError::CloudflareApi(format!(
|
||||
"HTTP {}: {}",
|
||||
status, body
|
||||
)));
|
||||
}
|
||||
|
||||
let zones: CloudflareZonesResponse = response.json().await?;
|
||||
@@ -95,17 +99,28 @@ impl CloudflareClient {
|
||||
if !zones.success {
|
||||
let errors = zones.errors.unwrap_or_default();
|
||||
let error_messages: Vec<String> = errors.iter().map(|e| e.message.clone()).collect();
|
||||
return Err(AcmeError::CloudflareApi(format!("API errors: {}", error_messages.join(", "))));
|
||||
return Err(AcmeError::CloudflareApi(format!(
|
||||
"API errors: {}",
|
||||
error_messages.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
zones.result
|
||||
zones
|
||||
.result
|
||||
.into_iter()
|
||||
.find(|z| z.name == domain)
|
||||
.map(|z| z.id)
|
||||
.ok_or_else(|| AcmeError::CloudflareApi(format!("Zone not found for domain: {}", domain)))
|
||||
.ok_or_else(|| {
|
||||
AcmeError::CloudflareApi(format!("Zone not found for domain: {}", domain))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_txt_record(&self, domain: &str, record_name: &str, content: &str) -> Result<String, AcmeError> {
|
||||
pub async fn create_txt_record(
|
||||
&self,
|
||||
domain: &str,
|
||||
record_name: &str,
|
||||
content: &str,
|
||||
) -> Result<String, AcmeError> {
|
||||
let zone_id = self.get_zone_id(domain).await?;
|
||||
info!("Creating TXT record {} in zone {}", record_name, domain);
|
||||
|
||||
@@ -116,9 +131,13 @@ impl CloudflareClient {
|
||||
ttl: 120, // 2 minutes TTL for quick propagation
|
||||
};
|
||||
|
||||
let url = format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records", zone_id);
|
||||
let url = format!(
|
||||
"https://api.cloudflare.com/client/v4/zones/{}/dns_records",
|
||||
zone_id
|
||||
);
|
||||
|
||||
let response = self.client
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_token))
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -129,7 +148,10 @@ impl CloudflareClient {
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(AcmeError::CloudflareApi(format!("Failed to create DNS record ({}): {}", status, body)));
|
||||
return Err(AcmeError::CloudflareApi(format!(
|
||||
"Failed to create DNS record ({}): {}",
|
||||
status, body
|
||||
)));
|
||||
}
|
||||
|
||||
let result: CreateDnsRecordResponse = response.json().await?;
|
||||
@@ -137,7 +159,10 @@ impl CloudflareClient {
|
||||
if !result.success {
|
||||
let errors = result.errors.unwrap_or_default();
|
||||
let error_messages: Vec<String> = errors.iter().map(|e| e.message.clone()).collect();
|
||||
return Err(AcmeError::CloudflareApi(format!("Failed to create record: {}", error_messages.join(", "))));
|
||||
return Err(AcmeError::CloudflareApi(format!(
|
||||
"Failed to create record: {}",
|
||||
error_messages.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
debug!("Created DNS record with ID: {}", result.result.id);
|
||||
@@ -148,9 +173,13 @@ impl CloudflareClient {
|
||||
let zone_id = self.get_zone_id(domain).await?;
|
||||
info!("Deleting TXT record {} from zone {}", record_id, domain);
|
||||
|
||||
let url = format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", zone_id, record_id);
|
||||
let url = format!(
|
||||
"https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}",
|
||||
zone_id, record_id
|
||||
);
|
||||
|
||||
let response = self.client
|
||||
let response = self
|
||||
.client
|
||||
.delete(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_token))
|
||||
.send()
|
||||
@@ -159,22 +188,30 @@ impl CloudflareClient {
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(AcmeError::CloudflareApi(format!("Failed to delete DNS record ({}): {}", status, body)));
|
||||
return Err(AcmeError::CloudflareApi(format!(
|
||||
"Failed to delete DNS record ({}): {}",
|
||||
status, body
|
||||
)));
|
||||
}
|
||||
|
||||
info!("Successfully deleted DNS record");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_txt_record(&self, domain: &str, record_name: &str) -> Result<Option<String>, AcmeError> {
|
||||
pub async fn find_txt_record(
|
||||
&self,
|
||||
domain: &str,
|
||||
record_name: &str,
|
||||
) -> Result<Option<String>, AcmeError> {
|
||||
let zone_id = self.get_zone_id(domain).await?;
|
||||
|
||||
|
||||
let url = format!(
|
||||
"https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=TXT&name={}",
|
||||
zone_id, record_name
|
||||
);
|
||||
|
||||
let response = self.client
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_token))
|
||||
.send()
|
||||
@@ -183,7 +220,10 @@ impl CloudflareClient {
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(AcmeError::CloudflareApi(format!("Failed to list DNS records ({}): {}", status, body)));
|
||||
return Err(AcmeError::CloudflareApi(format!(
|
||||
"Failed to list DNS records ({}): {}",
|
||||
status, body
|
||||
)));
|
||||
}
|
||||
|
||||
let records: CloudflareDnsRecordsResponse = response.json().await?;
|
||||
@@ -191,9 +231,12 @@ impl CloudflareClient {
|
||||
if !records.success {
|
||||
let errors = records.errors.unwrap_or_default();
|
||||
let error_messages: Vec<String> = errors.iter().map(|e| e.message.clone()).collect();
|
||||
return Err(AcmeError::CloudflareApi(format!("Failed to list records: {}", error_messages.join(", "))));
|
||||
return Err(AcmeError::CloudflareApi(format!(
|
||||
"Failed to list records: {}",
|
||||
error_messages.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(records.result.first().map(|r| r.id.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,37 +4,37 @@ use thiserror::Error;
|
||||
pub enum AcmeError {
|
||||
#[error("ACME account creation failed: {0}")]
|
||||
AccountCreation(String),
|
||||
|
||||
|
||||
#[error("ACME order creation failed: {0}")]
|
||||
OrderCreation(String),
|
||||
|
||||
|
||||
#[error("ACME challenge failed: {0}")]
|
||||
Challenge(String),
|
||||
|
||||
|
||||
#[error("DNS propagation timeout")]
|
||||
DnsPropagationTimeout,
|
||||
|
||||
|
||||
#[error("Certificate generation failed: {0}")]
|
||||
CertificateGeneration(String),
|
||||
|
||||
|
||||
#[error("Cloudflare API error: {0}")]
|
||||
CloudflareApi(String),
|
||||
|
||||
|
||||
#[error("DNS provider not found")]
|
||||
DnsProviderNotFound,
|
||||
|
||||
|
||||
#[error("Invalid domain: {0}")]
|
||||
InvalidDomain(String),
|
||||
|
||||
|
||||
#[error("HTTP request failed: {0}")]
|
||||
HttpRequest(#[from] reqwest::Error),
|
||||
|
||||
|
||||
#[error("JSON parsing failed: {0}")]
|
||||
JsonParsing(#[from] serde_json::Error),
|
||||
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
|
||||
#[error("Instant ACME error: {0}")]
|
||||
InstantAcme(String),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ pub mod error;
|
||||
|
||||
pub use client::AcmeClient;
|
||||
pub use cloudflare::CloudflareClient;
|
||||
pub use error::AcmeError;
|
||||
pub use error::AcmeError;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, SanType, KeyPair, PKCS_ECDSA_P256_SHA256};
|
||||
use rcgen::{
|
||||
Certificate, CertificateParams, DistinguishedName, DnType, KeyPair, SanType,
|
||||
PKCS_ECDSA_P256_SHA256,
|
||||
};
|
||||
use std::net::IpAddr;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::repository::DnsProviderRepository;
|
||||
use crate::database::entities::dns_provider::DnsProviderType;
|
||||
use crate::database::repository::DnsProviderRepository;
|
||||
use crate::services::acme::{AcmeClient, AcmeError};
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
@@ -19,7 +22,7 @@ impl CertificateService {
|
||||
pub fn new() -> Self {
|
||||
Self { db: None }
|
||||
}
|
||||
|
||||
|
||||
pub fn with_db(db: DatabaseConnection) -> Self {
|
||||
Self { db: Some(db) }
|
||||
}
|
||||
@@ -27,17 +30,17 @@ impl CertificateService {
|
||||
/// Generate self-signed certificate optimized for Xray
|
||||
pub async fn generate_self_signed(&self, domain: &str) -> anyhow::Result<(String, String)> {
|
||||
tracing::info!("Generating self-signed certificate for domain: {}", domain);
|
||||
|
||||
|
||||
// Create certificate parameters with ECDSA (recommended for Xray)
|
||||
let mut params = CertificateParams::new(vec![domain.to_string()]);
|
||||
|
||||
|
||||
// Use ECDSA P-256 which is recommended for Xray (equivalent to RSA-3072 in strength)
|
||||
params.alg = &PKCS_ECDSA_P256_SHA256;
|
||||
|
||||
|
||||
// Generate ECDSA key pair
|
||||
let key_pair = KeyPair::generate(&PKCS_ECDSA_P256_SHA256)?;
|
||||
params.key_pair = Some(key_pair);
|
||||
|
||||
|
||||
// Set certificate subject with proper fields
|
||||
let mut distinguished_name = DistinguishedName::new();
|
||||
distinguished_name.push(DnType::CommonName, domain);
|
||||
@@ -47,57 +50,60 @@ impl CertificateService {
|
||||
distinguished_name.push(DnType::StateOrProvinceName, "State");
|
||||
distinguished_name.push(DnType::LocalityName, "City");
|
||||
params.distinguished_name = distinguished_name;
|
||||
|
||||
|
||||
// Add comprehensive Subject Alternative Names for better compatibility
|
||||
let mut san_list = vec![
|
||||
SanType::DnsName(domain.to_string()),
|
||||
SanType::DnsName("localhost".to_string()),
|
||||
];
|
||||
|
||||
|
||||
// Add IP addresses if domain looks like an IP
|
||||
if let Ok(ip) = domain.parse::<IpAddr>() {
|
||||
san_list.push(SanType::IpAddress(ip));
|
||||
}
|
||||
|
||||
|
||||
// Always add localhost IP for local testing
|
||||
san_list.push(SanType::IpAddress(IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))));
|
||||
|
||||
san_list.push(SanType::IpAddress(IpAddr::V4(std::net::Ipv4Addr::new(
|
||||
127, 0, 0, 1,
|
||||
))));
|
||||
|
||||
// If domain is not an IP, also add wildcard subdomain
|
||||
if domain.parse::<IpAddr>().is_err() && !domain.starts_with("*.") {
|
||||
san_list.push(SanType::DnsName(format!("*.{}", domain)));
|
||||
}
|
||||
|
||||
|
||||
params.subject_alt_names = san_list;
|
||||
|
||||
|
||||
// Set validity period (1 year as recommended)
|
||||
params.not_before = OffsetDateTime::now_utc();
|
||||
params.not_after = OffsetDateTime::now_utc() + Duration::days(365);
|
||||
|
||||
|
||||
// Set serial number
|
||||
params.serial_number = Some(rcgen::SerialNumber::from_slice(&[1, 2, 3, 4]));
|
||||
|
||||
|
||||
// Generate certificate
|
||||
let cert = Certificate::from_params(params)?;
|
||||
|
||||
|
||||
// Get PEM format with proper formatting
|
||||
let cert_pem = cert.serialize_pem()?;
|
||||
let key_pem = cert.serialize_private_key_pem();
|
||||
|
||||
|
||||
// Validate PEM format
|
||||
if !cert_pem.starts_with("-----BEGIN CERTIFICATE-----") || !cert_pem.ends_with("-----END CERTIFICATE-----\n") {
|
||||
if !cert_pem.starts_with("-----BEGIN CERTIFICATE-----")
|
||||
|| !cert_pem.ends_with("-----END CERTIFICATE-----\n")
|
||||
{
|
||||
return Err(anyhow::anyhow!("Invalid certificate PEM format"));
|
||||
}
|
||||
|
||||
|
||||
if !key_pem.starts_with("-----BEGIN") || !key_pem.contains("PRIVATE KEY-----") {
|
||||
return Err(anyhow::anyhow!("Invalid private key PEM format"));
|
||||
}
|
||||
|
||||
|
||||
tracing::debug!("Generated ECDSA P-256 certificate for domain: {}", domain);
|
||||
|
||||
|
||||
Ok((cert_pem, key_pem))
|
||||
}
|
||||
|
||||
|
||||
/// Generate Let's Encrypt certificate using DNS challenge
|
||||
pub async fn generate_letsencrypt_certificate(
|
||||
&self,
|
||||
@@ -106,123 +112,148 @@ impl CertificateService {
|
||||
acme_email: &str,
|
||||
staging: bool,
|
||||
) -> Result<(String, String), AcmeError> {
|
||||
tracing::info!("Generating Let's Encrypt certificate for domain: {} using DNS challenge", domain);
|
||||
|
||||
tracing::info!(
|
||||
"Generating Let's Encrypt certificate for domain: {} using DNS challenge",
|
||||
domain
|
||||
);
|
||||
|
||||
// Get database connection
|
||||
let db = self.db.as_ref()
|
||||
let db = self
|
||||
.db
|
||||
.as_ref()
|
||||
.ok_or_else(|| AcmeError::DnsProviderNotFound)?;
|
||||
|
||||
|
||||
// Get DNS provider
|
||||
let dns_repo = DnsProviderRepository::new(db.clone());
|
||||
let dns_provider = dns_repo.find_by_id(dns_provider_id)
|
||||
let dns_provider = dns_repo
|
||||
.find_by_id(dns_provider_id)
|
||||
.await
|
||||
.map_err(|_| AcmeError::DnsProviderNotFound)?
|
||||
.ok_or_else(|| AcmeError::DnsProviderNotFound)?;
|
||||
|
||||
|
||||
// Verify provider is Cloudflare (only supported provider for now)
|
||||
if dns_provider.provider_type != DnsProviderType::Cloudflare.as_str() {
|
||||
return Err(AcmeError::CloudflareApi("Only Cloudflare provider is supported".to_string()));
|
||||
return Err(AcmeError::CloudflareApi(
|
||||
"Only Cloudflare provider is supported".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
if !dns_provider.is_active {
|
||||
return Err(AcmeError::DnsProviderNotFound);
|
||||
}
|
||||
|
||||
|
||||
// Determine ACME directory URL
|
||||
let directory_url = if staging {
|
||||
"https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
} else {
|
||||
"https://acme-v02.api.letsencrypt.org/directory"
|
||||
};
|
||||
|
||||
|
||||
// Create ACME client
|
||||
let mut acme_client = AcmeClient::new(
|
||||
dns_provider.api_token.clone(),
|
||||
acme_email,
|
||||
directory_url.to_string(),
|
||||
).await?;
|
||||
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Get base domain for DNS operations
|
||||
let base_domain = AcmeClient::get_base_domain(domain)?;
|
||||
|
||||
|
||||
// Generate certificate
|
||||
let (cert_pem, key_pem) = acme_client
|
||||
.get_certificate(domain, &base_domain)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Successfully generated Let's Encrypt certificate for domain: {}", domain);
|
||||
let (cert_pem, key_pem) = acme_client.get_certificate(domain, &base_domain).await?;
|
||||
|
||||
tracing::info!(
|
||||
"Successfully generated Let's Encrypt certificate for domain: {}",
|
||||
domain
|
||||
);
|
||||
Ok((cert_pem, key_pem))
|
||||
}
|
||||
|
||||
/// Renew certificate by ID (used for manual renewal)
|
||||
pub async fn renew_certificate_by_id(&self, cert_id: Uuid) -> anyhow::Result<(String, String)> {
|
||||
let db = self.db.as_ref()
|
||||
let db = self
|
||||
.db
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Database connection not available"))?;
|
||||
|
||||
|
||||
// Get the certificate from database
|
||||
let cert_repo = crate::database::repository::CertificateRepository::new(db.clone());
|
||||
let certificate = cert_repo.find_by_id(cert_id)
|
||||
let certificate = cert_repo
|
||||
.find_by_id(cert_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Certificate not found"))?;
|
||||
|
||||
tracing::info!("Renewing certificate '{}' for domain: {}", certificate.name, certificate.domain);
|
||||
|
||||
|
||||
tracing::info!(
|
||||
"Renewing certificate '{}' for domain: {}",
|
||||
certificate.name,
|
||||
certificate.domain
|
||||
);
|
||||
|
||||
match certificate.cert_type.as_str() {
|
||||
"letsencrypt" => {
|
||||
// For Let's Encrypt, we need to regenerate using ACME
|
||||
// Find an active Cloudflare DNS provider
|
||||
let dns_repo = crate::database::repository::DnsProviderRepository::new(db.clone());
|
||||
let providers = dns_repo.find_active_by_type("cloudflare").await?;
|
||||
|
||||
|
||||
if providers.is_empty() {
|
||||
return Err(anyhow::anyhow!("No active Cloudflare DNS provider found for Let's Encrypt renewal"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"No active Cloudflare DNS provider found for Let's Encrypt renewal"
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
let dns_provider = &providers[0];
|
||||
let acme_email = "admin@example.com"; // TODO: Store this with certificate
|
||||
|
||||
|
||||
// Generate new certificate
|
||||
let (cert_pem, key_pem) = self.generate_letsencrypt_certificate(
|
||||
&certificate.domain,
|
||||
dns_provider.id,
|
||||
acme_email,
|
||||
false, // Production
|
||||
).await?;
|
||||
|
||||
let (cert_pem, key_pem) = self
|
||||
.generate_letsencrypt_certificate(
|
||||
&certificate.domain,
|
||||
dns_provider.id,
|
||||
acme_email,
|
||||
false, // Production
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Update in database
|
||||
cert_repo.update_certificate_data(
|
||||
cert_id,
|
||||
&cert_pem,
|
||||
&key_pem,
|
||||
chrono::Utc::now() + chrono::Duration::days(90),
|
||||
).await?;
|
||||
|
||||
cert_repo
|
||||
.update_certificate_data(
|
||||
cert_id,
|
||||
&cert_pem,
|
||||
&key_pem,
|
||||
chrono::Utc::now() + chrono::Duration::days(90),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((cert_pem, key_pem))
|
||||
}
|
||||
"self_signed" => {
|
||||
// For self-signed, generate a new one
|
||||
let (cert_pem, key_pem) = self.generate_self_signed(&certificate.domain).await?;
|
||||
|
||||
|
||||
// Update in database
|
||||
cert_repo.update_certificate_data(
|
||||
cert_id,
|
||||
&cert_pem,
|
||||
&key_pem,
|
||||
chrono::Utc::now() + chrono::Duration::days(365),
|
||||
).await?;
|
||||
|
||||
cert_repo
|
||||
.update_certificate_data(
|
||||
cert_id,
|
||||
&cert_pem,
|
||||
&key_pem,
|
||||
chrono::Utc::now() + chrono::Duration::days(365),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((cert_pem, key_pem))
|
||||
}
|
||||
_ => {
|
||||
Err(anyhow::anyhow!("Cannot renew imported certificates automatically"))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Cannot renew imported certificates automatically"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Renew certificate (legacy method for backward compatibility)
|
||||
pub async fn renew_certificate(&self, domain: &str) -> anyhow::Result<(String, String)> {
|
||||
tracing::info!("Renewing certificate for domain: {}", domain);
|
||||
|
||||
|
||||
// For backward compatibility, just generate a new self-signed certificate
|
||||
self.generate_self_signed(domain).await
|
||||
}
|
||||
@@ -232,4 +263,4 @@ impl Default for CertificateService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SyncEvent {
|
||||
InboundChanged(Uuid), // server_id
|
||||
InboundChanged(Uuid), // server_id
|
||||
UserAccessChanged(Uuid), // server_id
|
||||
}
|
||||
|
||||
@@ -27,4 +27,4 @@ pub fn send_sync_event(event: SyncEvent) {
|
||||
} else {
|
||||
tracing::error!("Event bus not initialized");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
pub mod xray;
|
||||
pub mod acme;
|
||||
pub mod certificates;
|
||||
pub mod events;
|
||||
pub mod tasks;
|
||||
pub mod uri_generator;
|
||||
pub mod telegram;
|
||||
pub mod uri_generator;
|
||||
pub mod xray;
|
||||
|
||||
pub use xray::XrayService;
|
||||
pub use tasks::TaskScheduler;
|
||||
pub use uri_generator::UriGeneratorService;
|
||||
pub use certificates::CertificateService;
|
||||
pub use telegram::TelegramService;
|
||||
pub use tasks::TaskScheduler;
|
||||
pub use telegram::TelegramService;
|
||||
pub use uri_generator::UriGeneratorService;
|
||||
pub use xray::XrayService;
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
use anyhow::Result;
|
||||
use tokio_cron_scheduler::{JobScheduler, Job};
|
||||
use tracing::{info, error, warn, debug};
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::database::repository::{ServerRepository, ServerInboundRepository, InboundTemplateRepository, InboundUsersRepository, CertificateRepository, UserRepository};
|
||||
use crate::database::entities::inbound_users;
|
||||
use crate::services::XrayService;
|
||||
use crate::database::repository::{
|
||||
CertificateRepository, InboundTemplateRepository, InboundUsersRepository,
|
||||
ServerInboundRepository, ServerRepository, UserRepository,
|
||||
};
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::services::events::SyncEvent;
|
||||
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, RelationTrait, JoinType};
|
||||
use uuid::Uuid;
|
||||
use crate::services::XrayService;
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, JoinType, QueryFilter, RelationTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct TaskScheduler {
|
||||
scheduler: JobScheduler,
|
||||
@@ -47,7 +50,10 @@ impl TaskScheduler {
|
||||
pub async fn new() -> Result<Self> {
|
||||
let scheduler = JobScheduler::new().await?;
|
||||
let task_status = Arc::new(RwLock::new(HashMap::new()));
|
||||
Ok(Self { scheduler, task_status })
|
||||
Ok(Self {
|
||||
scheduler,
|
||||
task_status,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get current status of all tasks
|
||||
@@ -56,15 +62,20 @@ impl TaskScheduler {
|
||||
}
|
||||
|
||||
/// Start event-driven sync handler
|
||||
pub async fn start_event_handler(db: DatabaseManager, mut event_receiver: tokio::sync::broadcast::Receiver<SyncEvent>) {
|
||||
pub async fn start_event_handler(
|
||||
db: DatabaseManager,
|
||||
mut event_receiver: tokio::sync::broadcast::Receiver<SyncEvent>,
|
||||
) {
|
||||
let xray_service = XrayService::new();
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
||||
while let Ok(event) = event_receiver.recv().await {
|
||||
match event {
|
||||
SyncEvent::InboundChanged(server_id) | SyncEvent::UserAccessChanged(server_id) => {
|
||||
if let Err(e) = sync_single_server_by_id(&xray_service, &db, server_id).await {
|
||||
SyncEvent::InboundChanged(server_id)
|
||||
| SyncEvent::UserAccessChanged(server_id) => {
|
||||
if let Err(e) =
|
||||
sync_single_server_by_id(&xray_service, &db, server_id).await
|
||||
{
|
||||
error!("Failed to sync server {} from event: {}", server_id, e);
|
||||
}
|
||||
}
|
||||
@@ -74,34 +85,36 @@ impl TaskScheduler {
|
||||
}
|
||||
|
||||
pub async fn start(&mut self, db: DatabaseManager, xray_service: XrayService) -> Result<()> {
|
||||
|
||||
// Initialize task status
|
||||
{
|
||||
let mut status = self.task_status.write().unwrap();
|
||||
status.insert("xray_sync".to_string(), TaskStatus {
|
||||
name: "Xray Synchronization".to_string(),
|
||||
description: "Synchronizes database state with xray servers".to_string(),
|
||||
schedule: "0 * * * * * (every minute)".to_string(),
|
||||
status: TaskState::Idle,
|
||||
last_run: None,
|
||||
next_run: Some(Utc::now() + chrono::Duration::minutes(1)),
|
||||
total_runs: 0,
|
||||
success_count: 0,
|
||||
error_count: 0,
|
||||
last_error: None,
|
||||
last_duration_ms: None,
|
||||
});
|
||||
status.insert(
|
||||
"xray_sync".to_string(),
|
||||
TaskStatus {
|
||||
name: "Xray Synchronization".to_string(),
|
||||
description: "Synchronizes database state with xray servers".to_string(),
|
||||
schedule: "0 * * * * * (every minute)".to_string(),
|
||||
status: TaskState::Idle,
|
||||
last_run: None,
|
||||
next_run: Some(Utc::now() + chrono::Duration::minutes(1)),
|
||||
total_runs: 0,
|
||||
success_count: 0,
|
||||
error_count: 0,
|
||||
last_error: None,
|
||||
last_duration_ms: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Run initial sync in background to avoid blocking startup
|
||||
let db_initial = db.clone();
|
||||
let xray_service_initial = xray_service.clone();
|
||||
let task_status_initial = self.task_status.clone();
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
info!("Starting initial xray sync in background...");
|
||||
let start_time = Utc::now();
|
||||
|
||||
|
||||
// Update status to running
|
||||
{
|
||||
let mut status = task_status_initial.write().unwrap();
|
||||
@@ -111,7 +124,7 @@ impl TaskScheduler {
|
||||
task.total_runs += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
match sync_xray_state(db_initial, xray_service_initial).await {
|
||||
Ok(_) => {
|
||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||
@@ -123,7 +136,7 @@ impl TaskScheduler {
|
||||
task.last_error = None;
|
||||
}
|
||||
info!("Initial xray sync completed successfully in {}ms", duration);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||
let mut status = task_status_initial.write().unwrap();
|
||||
@@ -137,20 +150,20 @@ impl TaskScheduler {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add synchronization task that runs every minute
|
||||
let db_clone = db.clone();
|
||||
let xray_service_clone = xray_service.clone();
|
||||
let task_status_clone = self.task_status.clone();
|
||||
|
||||
|
||||
let sync_job = Job::new_async("0 */5 * * * *", move |_uuid, _l| {
|
||||
let db = db_clone.clone();
|
||||
let xray_service = xray_service_clone.clone();
|
||||
let task_status = task_status_clone.clone();
|
||||
|
||||
|
||||
Box::pin(async move {
|
||||
let start_time = Utc::now();
|
||||
|
||||
|
||||
// Update status to running
|
||||
{
|
||||
let mut status = task_status.write().unwrap();
|
||||
@@ -161,7 +174,7 @@ impl TaskScheduler {
|
||||
task.next_run = Some(start_time + chrono::Duration::minutes(1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
match sync_xray_state(db, xray_service).await {
|
||||
Ok(_) => {
|
||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||
@@ -172,7 +185,7 @@ impl TaskScheduler {
|
||||
task.last_duration_ms = Some(duration);
|
||||
task.last_error = None;
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||
let mut status = task_status.write().unwrap();
|
||||
@@ -187,38 +200,42 @@ impl TaskScheduler {
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
|
||||
self.scheduler.add(sync_job).await?;
|
||||
|
||||
|
||||
// Add certificate renewal task that runs once a day at 2 AM
|
||||
let db_clone_cert = db.clone();
|
||||
let task_status_cert = self.task_status.clone();
|
||||
|
||||
|
||||
// Initialize certificate renewal task status
|
||||
{
|
||||
let mut status = self.task_status.write().unwrap();
|
||||
status.insert("cert_renewal".to_string(), TaskStatus {
|
||||
name: "Certificate Renewal".to_string(),
|
||||
description: "Renews Let's Encrypt certificates that expire within 15 days".to_string(),
|
||||
schedule: "0 0 2 * * * (daily at 2 AM)".to_string(),
|
||||
status: TaskState::Idle,
|
||||
last_run: None,
|
||||
next_run: Some(Utc::now() + chrono::Duration::days(1)),
|
||||
total_runs: 0,
|
||||
success_count: 0,
|
||||
error_count: 0,
|
||||
last_error: None,
|
||||
last_duration_ms: None,
|
||||
});
|
||||
status.insert(
|
||||
"cert_renewal".to_string(),
|
||||
TaskStatus {
|
||||
name: "Certificate Renewal".to_string(),
|
||||
description: "Renews Let's Encrypt certificates that expire within 15 days"
|
||||
.to_string(),
|
||||
schedule: "0 0 2 * * * (daily at 2 AM)".to_string(),
|
||||
status: TaskState::Idle,
|
||||
last_run: None,
|
||||
next_run: Some(Utc::now() + chrono::Duration::days(1)),
|
||||
total_runs: 0,
|
||||
success_count: 0,
|
||||
error_count: 0,
|
||||
last_error: None,
|
||||
last_duration_ms: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
let cert_renewal_job = Job::new_async("0 0 2 * * *", move |_uuid, _l| {
|
||||
let db = db_clone_cert.clone();
|
||||
let task_status = task_status_cert.clone();
|
||||
|
||||
|
||||
Box::pin(async move {
|
||||
let start_time = Utc::now();
|
||||
|
||||
|
||||
// Update task status to running
|
||||
{
|
||||
let mut status = task_status.write().unwrap();
|
||||
@@ -228,7 +245,7 @@ impl TaskScheduler {
|
||||
task.total_runs += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
match check_and_renew_certificates(&db).await {
|
||||
Ok(_) => {
|
||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||
@@ -239,7 +256,7 @@ impl TaskScheduler {
|
||||
task.last_duration_ms = Some(duration);
|
||||
task.last_error = None;
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||
let mut status = task_status.write().unwrap();
|
||||
@@ -254,9 +271,9 @@ impl TaskScheduler {
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
|
||||
self.scheduler.add(cert_renewal_job).await?;
|
||||
|
||||
|
||||
// Also run certificate check on startup
|
||||
info!("Running initial certificate renewal check...");
|
||||
tokio::spawn(async move {
|
||||
@@ -264,7 +281,7 @@ impl TaskScheduler {
|
||||
error!("Initial certificate renewal check failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
self.scheduler.start().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -281,7 +298,12 @@ impl TaskScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_task_status_with_error(&self, task_id: &str, error: String, duration_ms: Option<u64>) {
|
||||
fn update_task_status_with_error(
|
||||
&self,
|
||||
task_id: &str,
|
||||
error: String,
|
||||
duration_ms: Option<u64>,
|
||||
) {
|
||||
let mut status = self.task_status.write().unwrap();
|
||||
if let Some(task) = status.get_mut(task_id) {
|
||||
task.status = TaskState::Error;
|
||||
@@ -301,11 +323,10 @@ impl TaskScheduler {
|
||||
|
||||
/// Synchronize xray server state with database state
|
||||
async fn sync_xray_state(db: DatabaseManager, xray_service: XrayService) -> Result<()> {
|
||||
|
||||
let server_repo = ServerRepository::new(db.connection().clone());
|
||||
let inbound_repo = ServerInboundRepository::new(db.connection().clone());
|
||||
let template_repo = InboundTemplateRepository::new(db.connection().clone());
|
||||
|
||||
|
||||
// Get all servers from database
|
||||
let servers = match server_repo.find_all().await {
|
||||
Ok(servers) => servers,
|
||||
@@ -314,50 +335,50 @@ async fn sync_xray_state(db: DatabaseManager, xray_service: XrayService) -> Resu
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
for server in servers {
|
||||
|
||||
let endpoint = server.get_grpc_endpoint();
|
||||
|
||||
|
||||
// Test connection first
|
||||
match xray_service.test_connection(server.id, &endpoint).await {
|
||||
Ok(false) => {
|
||||
warn!("Cannot connect to server {} at {}, skipping", server.name, endpoint);
|
||||
warn!(
|
||||
"Cannot connect to server {} at {}, skipping",
|
||||
server.name, endpoint
|
||||
);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error testing connection to server {}: {}", server.name, e);
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
// Get desired inbounds from database
|
||||
let desired_inbounds = match get_desired_inbounds_from_db(&db, &server, &inbound_repo, &template_repo).await {
|
||||
Ok(inbounds) => inbounds,
|
||||
Err(e) => {
|
||||
error!("Failed to get desired inbounds for server {}: {}", server.name, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let desired_inbounds =
|
||||
match get_desired_inbounds_from_db(&db, &server, &inbound_repo, &template_repo).await {
|
||||
Ok(inbounds) => inbounds,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to get desired inbounds for server {}: {}",
|
||||
server.name, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Synchronize inbounds
|
||||
if let Err(e) = sync_server_inbounds(
|
||||
&xray_service,
|
||||
server.id,
|
||||
&endpoint,
|
||||
&desired_inbounds
|
||||
).await {
|
||||
if let Err(e) =
|
||||
sync_server_inbounds(&xray_service, server.id, &endpoint, &desired_inbounds).await
|
||||
{
|
||||
error!("Failed to sync inbounds for server {}: {}", server.name, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Get desired inbounds configuration from database
|
||||
async fn get_desired_inbounds_from_db(
|
||||
db: &DatabaseManager,
|
||||
@@ -365,38 +386,47 @@ async fn get_desired_inbounds_from_db(
|
||||
inbound_repo: &ServerInboundRepository,
|
||||
template_repo: &InboundTemplateRepository,
|
||||
) -> Result<HashMap<String, DesiredInbound>> {
|
||||
|
||||
// Get all inbounds for this server
|
||||
let inbounds = inbound_repo.find_by_server_id(server.id).await?;
|
||||
let mut desired_inbounds = HashMap::new();
|
||||
|
||||
|
||||
for inbound in inbounds {
|
||||
// Get template for this inbound
|
||||
let template = match template_repo.find_by_id(inbound.template_id).await? {
|
||||
Some(template) => template,
|
||||
None => {
|
||||
warn!("Template {} not found for inbound {}, skipping", inbound.template_id, inbound.tag);
|
||||
warn!(
|
||||
"Template {} not found for inbound {}, skipping",
|
||||
inbound.template_id, inbound.tag
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Get users for this inbound
|
||||
let users = get_users_for_inbound(db, inbound.id).await?;
|
||||
|
||||
|
||||
|
||||
// Get port from template or override
|
||||
let port = inbound.port_override.unwrap_or(template.default_port);
|
||||
|
||||
|
||||
// Get certificate if specified
|
||||
let (cert_pem, key_pem) = if let Some(cert_id) = inbound.certificate_id {
|
||||
match load_certificate_from_db(db, inbound.certificate_id).await {
|
||||
Ok((cert, key)) => {
|
||||
info!("Loaded certificate {} for inbound {}, has_cert={}, has_key={}",
|
||||
cert_id, inbound.tag, cert.is_some(), key.is_some());
|
||||
info!(
|
||||
"Loaded certificate {} for inbound {}, has_cert={}, has_key={}",
|
||||
cert_id,
|
||||
inbound.tag,
|
||||
cert.is_some(),
|
||||
key.is_some()
|
||||
);
|
||||
(cert, key)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to load certificate {} for inbound {}: {}", cert_id, inbound.tag, e);
|
||||
warn!(
|
||||
"Failed to load certificate {} for inbound {}: {}",
|
||||
cert_id, inbound.tag, e
|
||||
);
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
@@ -404,7 +434,7 @@ async fn get_desired_inbounds_from_db(
|
||||
debug!("No certificate configured for inbound {}", inbound.tag);
|
||||
(None, None)
|
||||
};
|
||||
|
||||
|
||||
let desired_inbound = DesiredInbound {
|
||||
tag: inbound.tag.clone(),
|
||||
port,
|
||||
@@ -415,22 +445,24 @@ async fn get_desired_inbounds_from_db(
|
||||
cert_pem,
|
||||
key_pem,
|
||||
};
|
||||
|
||||
|
||||
desired_inbounds.insert(inbound.tag.clone(), desired_inbound);
|
||||
}
|
||||
|
||||
|
||||
Ok(desired_inbounds)
|
||||
}
|
||||
|
||||
/// Get users for specific inbound from database
|
||||
async fn get_users_for_inbound(db: &DatabaseManager, inbound_id: Uuid) -> Result<Vec<XrayUser>> {
|
||||
let inbound_users_repo = InboundUsersRepository::new(db.connection().clone());
|
||||
|
||||
let inbound_users = inbound_users_repo.find_active_by_inbound_id(inbound_id).await?;
|
||||
|
||||
|
||||
let inbound_users = inbound_users_repo
|
||||
.find_active_by_inbound_id(inbound_id)
|
||||
.await?;
|
||||
|
||||
// Get user details to generate emails
|
||||
let user_repo = UserRepository::new(db.connection().clone());
|
||||
|
||||
|
||||
let mut users: Vec<XrayUser> = Vec::new();
|
||||
for inbound_user in inbound_users {
|
||||
if let Some(user) = user_repo.find_by_id(inbound_user.user_id).await? {
|
||||
@@ -442,23 +474,24 @@ async fn get_users_for_inbound(db: &DatabaseManager, inbound_id: Uuid) -> Result
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
/// Load certificate from database
|
||||
async fn load_certificate_from_db(db: &DatabaseManager, cert_id: Option<Uuid>) -> Result<(Option<String>, Option<String>)> {
|
||||
async fn load_certificate_from_db(
|
||||
db: &DatabaseManager,
|
||||
cert_id: Option<Uuid>,
|
||||
) -> Result<(Option<String>, Option<String>)> {
|
||||
let cert_id = match cert_id {
|
||||
Some(id) => id,
|
||||
None => return Ok((None, None)),
|
||||
};
|
||||
|
||||
|
||||
let cert_repo = CertificateRepository::new(db.connection().clone());
|
||||
|
||||
|
||||
match cert_repo.find_by_id(cert_id).await? {
|
||||
Some(cert) => {
|
||||
Ok((Some(cert.certificate_pem()), Some(cert.private_key_pem())))
|
||||
},
|
||||
Some(cert) => Ok((Some(cert.certificate_pem()), Some(cert.private_key_pem()))),
|
||||
None => {
|
||||
warn!("Certificate {} not found", cert_id);
|
||||
Ok((None, None))
|
||||
@@ -474,7 +507,9 @@ async fn sync_server_inbounds(
|
||||
desired_inbounds: &HashMap<String, DesiredInbound>,
|
||||
) -> Result<()> {
|
||||
// Use optimized batch sync with single client
|
||||
xray_service.sync_server_inbounds_optimized(server_id, endpoint, desired_inbounds).await
|
||||
xray_service
|
||||
.sync_server_inbounds_optimized(server_id, endpoint, desired_inbounds)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sync a single server by ID (for event-driven sync)
|
||||
@@ -486,7 +521,7 @@ async fn sync_single_server_by_id(
|
||||
let server_repo = ServerRepository::new(db.connection().clone());
|
||||
let inbound_repo = ServerInboundRepository::new(db.connection().clone());
|
||||
let template_repo = InboundTemplateRepository::new(db.connection().clone());
|
||||
|
||||
|
||||
// Get server
|
||||
let server = match server_repo.find_by_id(server_id).await? {
|
||||
Some(server) => server,
|
||||
@@ -495,22 +530,22 @@ async fn sync_single_server_by_id(
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// For now, sync all servers (can add active/inactive flag later)
|
||||
|
||||
|
||||
// Get desired inbounds from database
|
||||
let desired_inbounds = get_desired_inbounds_from_db(db, &server, &inbound_repo, &template_repo).await?;
|
||||
|
||||
let desired_inbounds =
|
||||
get_desired_inbounds_from_db(db, &server, &inbound_repo, &template_repo).await?;
|
||||
|
||||
// Build endpoint
|
||||
let endpoint = server.get_grpc_endpoint();
|
||||
|
||||
|
||||
// Sync server
|
||||
sync_server_inbounds(xray_service, server_id, &endpoint, &desired_inbounds).await?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Represents desired inbound configuration from database
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DesiredInbound {
|
||||
@@ -534,73 +569,79 @@ pub struct XrayUser {
|
||||
|
||||
/// Check and renew certificates that expire within 15 days
|
||||
async fn check_and_renew_certificates(db: &DatabaseManager) -> Result<()> {
|
||||
use crate::services::certificates::CertificateService;
|
||||
use crate::database::repository::DnsProviderRepository;
|
||||
|
||||
use crate::services::certificates::CertificateService;
|
||||
|
||||
info!("Starting certificate renewal check...");
|
||||
|
||||
|
||||
let cert_repo = CertificateRepository::new(db.connection().clone());
|
||||
let dns_repo = DnsProviderRepository::new(db.connection().clone());
|
||||
let cert_service = CertificateService::with_db(db.connection().clone());
|
||||
|
||||
|
||||
// Get all certificates
|
||||
let certificates = cert_repo.find_all().await?;
|
||||
let mut renewed_count = 0;
|
||||
let mut checked_count = 0;
|
||||
|
||||
|
||||
for cert in certificates {
|
||||
// Only check Let's Encrypt certificates with auto_renew enabled
|
||||
if cert.cert_type != "letsencrypt" || !cert.auto_renew {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
checked_count += 1;
|
||||
|
||||
|
||||
// Check if certificate expires within 15 days
|
||||
if cert.expires_soon(15) {
|
||||
info!(
|
||||
"Certificate '{}' (ID: {}) expires at {} - renewing...",
|
||||
"Certificate '{}' (ID: {}) expires at {} - renewing...",
|
||||
cert.name, cert.id, cert.expires_at
|
||||
);
|
||||
|
||||
|
||||
// Find the DNS provider used for this certificate
|
||||
// For now, we'll use the first active Cloudflare provider
|
||||
// In production, you might want to store the provider ID with the certificate
|
||||
let providers = dns_repo.find_active_by_type("cloudflare").await?;
|
||||
|
||||
|
||||
if providers.is_empty() {
|
||||
error!(
|
||||
"Cannot renew certificate '{}': No active Cloudflare DNS provider found",
|
||||
"Cannot renew certificate '{}': No active Cloudflare DNS provider found",
|
||||
cert.name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
let dns_provider = &providers[0];
|
||||
|
||||
|
||||
// Need to get the ACME email - for now using a default
|
||||
// In production, this should be stored with the certificate
|
||||
let acme_email = "admin@example.com"; // TODO: Store this with certificate
|
||||
|
||||
|
||||
// Attempt to renew the certificate
|
||||
match cert_service.generate_letsencrypt_certificate(
|
||||
&cert.domain,
|
||||
dns_provider.id,
|
||||
acme_email,
|
||||
false, // Use production Let's Encrypt
|
||||
).await {
|
||||
match cert_service
|
||||
.generate_letsencrypt_certificate(
|
||||
&cert.domain,
|
||||
dns_provider.id,
|
||||
acme_email,
|
||||
false, // Use production Let's Encrypt
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((new_cert_pem, new_key_pem)) => {
|
||||
// Update the certificate in database
|
||||
match cert_repo.update_certificate_data(
|
||||
cert.id,
|
||||
&new_cert_pem,
|
||||
&new_key_pem,
|
||||
chrono::Utc::now() + chrono::Duration::days(90), // Let's Encrypt certs are valid for 90 days
|
||||
).await {
|
||||
match cert_repo
|
||||
.update_certificate_data(
|
||||
cert.id,
|
||||
&new_cert_pem,
|
||||
&new_key_pem,
|
||||
chrono::Utc::now() + chrono::Duration::days(90), // Let's Encrypt certs are valid for 90 days
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Successfully renewed certificate '{}'", cert.name);
|
||||
renewed_count += 1;
|
||||
|
||||
|
||||
// Trigger sync for all servers using this certificate
|
||||
// This will be done via the event system
|
||||
if let Err(e) = trigger_cert_renewal_sync(db, cert.id).await {
|
||||
@@ -608,7 +649,10 @@ async fn check_and_renew_certificates(db: &DatabaseManager) -> Result<()> {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to save renewed certificate '{}' to database: {}", cert.name, e);
|
||||
error!(
|
||||
"Failed to save renewed certificate '{}' to database: {}",
|
||||
cert.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -618,17 +662,17 @@ async fn check_and_renew_certificates(db: &DatabaseManager) -> Result<()> {
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
"Certificate '{}' expires at {} - no renewal needed yet",
|
||||
"Certificate '{}' expires at {} - no renewal needed yet",
|
||||
cert.name, cert.expires_at
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
info!(
|
||||
"Certificate renewal check completed: checked {}, renewed {}",
|
||||
"Certificate renewal check completed: checked {}, renewed {}",
|
||||
checked_count, renewed_count
|
||||
);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -636,23 +680,26 @@ async fn check_and_renew_certificates(db: &DatabaseManager) -> Result<()> {
|
||||
async fn trigger_cert_renewal_sync(db: &DatabaseManager, cert_id: Uuid) -> Result<()> {
|
||||
use crate::services::events::send_sync_event;
|
||||
use crate::services::events::SyncEvent;
|
||||
|
||||
|
||||
let inbound_repo = ServerInboundRepository::new(db.connection().clone());
|
||||
|
||||
|
||||
// Find all server inbounds that use this certificate
|
||||
let inbounds = inbound_repo.find_by_certificate_id(cert_id).await?;
|
||||
|
||||
|
||||
// Collect unique server IDs
|
||||
let mut server_ids = std::collections::HashSet::new();
|
||||
for inbound in inbounds {
|
||||
server_ids.insert(inbound.server_id);
|
||||
}
|
||||
|
||||
|
||||
// Trigger sync for each server
|
||||
for server_id in server_ids {
|
||||
info!("Triggering sync for server {} after certificate renewal", server_id);
|
||||
info!(
|
||||
"Triggering sync for server {} after certificate renewal",
|
||||
server_id
|
||||
);
|
||||
send_sync_event(SyncEvent::InboundChanged(server_id));
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use teloxide::{Bot, prelude::*};
|
||||
use teloxide::{prelude::*, Bot};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::config::AppConfig;
|
||||
use super::handlers::{self, Command};
|
||||
use crate::config::AppConfig;
|
||||
use crate::database::DatabaseManager;
|
||||
|
||||
/// Run the bot polling loop
|
||||
pub async fn run_polling(
|
||||
@@ -20,16 +20,11 @@ pub async fn run_polling(
|
||||
.branch(
|
||||
dptree::entry()
|
||||
.filter_command::<Command>()
|
||||
.endpoint(handlers::handle_command)
|
||||
)
|
||||
.branch(
|
||||
dptree::endpoint(handlers::handle_message)
|
||||
.endpoint(handlers::handle_command),
|
||||
)
|
||||
.branch(dptree::endpoint(handlers::handle_message)),
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query()
|
||||
.endpoint(handlers::handle_callback_query)
|
||||
);
|
||||
.branch(Update::filter_callback_query().endpoint(handlers::handle_callback_query));
|
||||
|
||||
let mut dispatcher = Dispatcher::builder(bot.clone(), handler)
|
||||
.dependencies(dptree::deps![db, app_config])
|
||||
@@ -45,4 +40,4 @@ pub async fn run_polling(
|
||||
tracing::info!("Telegram bot received shutdown signal");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,4 +43,4 @@ impl From<anyhow::Error> for TelegramError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self::Other(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,15 @@
|
||||
pub mod admin;
|
||||
pub mod user;
|
||||
pub mod types;
|
||||
pub mod user;
|
||||
|
||||
// Re-export main handler functions for easier access
|
||||
pub use admin::*;
|
||||
pub use user::*;
|
||||
pub use types::*;
|
||||
pub use user::*;
|
||||
|
||||
use teloxide::{prelude::*, types::CallbackQuery};
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::config::AppConfig;
|
||||
use crate::database::DatabaseManager;
|
||||
use teloxide::{prelude::*, types::CallbackQuery};
|
||||
|
||||
/// Handle bot commands
|
||||
pub async fn handle_command(
|
||||
@@ -30,44 +30,62 @@ pub async fn handle_command(
|
||||
}
|
||||
Command::Requests => {
|
||||
// Check if user is admin
|
||||
if user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
if user_repo
|
||||
.is_telegram_id_admin(telegram_id)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// Create a fake callback query for admin requests
|
||||
// This is a workaround since the admin_requests function expects a callback query
|
||||
// In practice, we could refactor this to not need a callback query
|
||||
tracing::info!("Admin {} requested to view requests", telegram_id);
|
||||
|
||||
|
||||
let message = "📋 Use the inline keyboard to view recent requests.";
|
||||
let keyboard = teloxide::types::InlineKeyboardMarkup::new(vec![
|
||||
vec![teloxide::types::InlineKeyboardButton::callback("📋 Recent Requests", "admin_requests")],
|
||||
]);
|
||||
|
||||
let keyboard = teloxide::types::InlineKeyboardMarkup::new(vec![vec![
|
||||
teloxide::types::InlineKeyboardButton::callback(
|
||||
"📋 Recent Requests",
|
||||
"admin_requests",
|
||||
),
|
||||
]]);
|
||||
|
||||
bot.send_message(chat_id, message)
|
||||
.reply_markup(keyboard)
|
||||
.await?;
|
||||
} else {
|
||||
let lang = get_user_language(from);
|
||||
let l10n = super::localization::LocalizationService::new();
|
||||
bot.send_message(chat_id, l10n.get(lang, "unauthorized")).await?;
|
||||
bot.send_message(chat_id, l10n.get(lang, "unauthorized"))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Command::Stats => {
|
||||
// Check if user is admin
|
||||
if user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
if user_repo
|
||||
.is_telegram_id_admin(telegram_id)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
handle_stats(bot, chat_id, &db).await?;
|
||||
} else {
|
||||
let lang = get_user_language(from);
|
||||
let l10n = super::localization::LocalizationService::new();
|
||||
bot.send_message(chat_id, l10n.get(lang, "unauthorized")).await?;
|
||||
bot.send_message(chat_id, l10n.get(lang, "unauthorized"))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Command::Broadcast { message } => {
|
||||
// Check if user is admin
|
||||
if user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
if user_repo
|
||||
.is_telegram_id_admin(telegram_id)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
handle_broadcast(bot, chat_id, message, &user_repo).await?;
|
||||
} else {
|
||||
let lang = get_user_language(from);
|
||||
let l10n = super::localization::LocalizationService::new();
|
||||
bot.send_message(chat_id, l10n.get(lang, "unauthorized")).await?;
|
||||
bot.send_message(chat_id, l10n.get(lang, "unauthorized"))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,68 +118,120 @@ pub async fn handle_callback_query(
|
||||
db: DatabaseManager,
|
||||
app_config: AppConfig,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Some(data) = &q.data {
|
||||
if let Some(callback_data) = CallbackData::parse(data) {
|
||||
match callback_data {
|
||||
CallbackData::RequestAccess => {
|
||||
handle_request_access(bot, &q, &db).await?;
|
||||
}
|
||||
CallbackData::MyConfigs => {
|
||||
handle_my_configs_edit(bot, &q, &db).await?;
|
||||
}
|
||||
CallbackData::SubscriptionLink => {
|
||||
handle_subscription_link(bot, &q, &db, &app_config).await?;
|
||||
}
|
||||
CallbackData::Support => {
|
||||
handle_support(bot, &q).await?;
|
||||
}
|
||||
CallbackData::AdminRequests => {
|
||||
handle_admin_requests_edit(bot, &q, &db).await?;
|
||||
}
|
||||
CallbackData::ApproveRequest(request_id) => {
|
||||
handle_approve_request(bot, &q, &request_id, &db).await?;
|
||||
}
|
||||
CallbackData::DeclineRequest(request_id) => {
|
||||
handle_decline_request(bot, &q, &request_id, &db).await?;
|
||||
}
|
||||
CallbackData::ViewRequest(request_id) => {
|
||||
handle_view_request(bot, &q, &request_id, &db).await?;
|
||||
}
|
||||
CallbackData::ShowServerConfigs(encoded_server_name) => {
|
||||
handle_show_server_configs(bot, &q, &encoded_server_name, &db).await?;
|
||||
}
|
||||
CallbackData::SelectServerAccess(request_id) => {
|
||||
// The request_id is now the full UUID from the mapping
|
||||
let short_id = types::generate_short_request_id(&request_id);
|
||||
handle_select_server_access(bot, &q, &short_id, &db).await?;
|
||||
}
|
||||
CallbackData::ToggleServer(request_id, server_id) => {
|
||||
// Both IDs are now full UUIDs from the mapping
|
||||
let short_request_id = types::generate_short_request_id(&request_id);
|
||||
let short_server_id = types::generate_short_server_id(&server_id);
|
||||
handle_toggle_server(bot, &q, &short_request_id, &short_server_id, &db).await?;
|
||||
}
|
||||
CallbackData::ApplyServerAccess(request_id) => {
|
||||
// The request_id is now the full UUID from the mapping
|
||||
let short_id = types::generate_short_request_id(&request_id);
|
||||
handle_apply_server_access(bot, &q, &short_id, &db).await?;
|
||||
}
|
||||
CallbackData::Back => {
|
||||
// Back to main menu - edit the existing message
|
||||
handle_start_edit(bot, &q, &db).await?;
|
||||
}
|
||||
CallbackData::BackToConfigs => {
|
||||
handle_my_configs_edit(bot, &q, &db).await?;
|
||||
}
|
||||
CallbackData::BackToRequests => {
|
||||
handle_admin_requests_edit(bot, &q, &db).await?;
|
||||
// Wrap all callback handling in a try-catch to send main menu on any error
|
||||
let result = async {
|
||||
if let Some(data) = &q.data {
|
||||
if let Some(callback_data) = CallbackData::parse(data) {
|
||||
match callback_data {
|
||||
CallbackData::RequestAccess => {
|
||||
handle_request_access(bot.clone(), &q, &db).await?;
|
||||
}
|
||||
CallbackData::MyConfigs => {
|
||||
handle_my_configs_edit(bot.clone(), &q, &db).await?;
|
||||
}
|
||||
CallbackData::SubscriptionLink => {
|
||||
handle_subscription_link(bot.clone(), &q, &db, &app_config).await?;
|
||||
}
|
||||
CallbackData::Support => {
|
||||
handle_support(bot.clone(), &q).await?;
|
||||
}
|
||||
CallbackData::AdminRequests => {
|
||||
handle_admin_requests_edit(bot.clone(), &q, &db).await?;
|
||||
}
|
||||
CallbackData::RequestList(page) => {
|
||||
handle_request_list(bot.clone(), &q, &db, page).await?;
|
||||
}
|
||||
CallbackData::ApproveRequest(request_id) => {
|
||||
handle_approve_request(bot.clone(), &q, &request_id, &db).await?;
|
||||
}
|
||||
CallbackData::DeclineRequest(request_id) => {
|
||||
handle_decline_request(bot.clone(), &q, &request_id, &db).await?;
|
||||
}
|
||||
CallbackData::ViewRequest(request_id) => {
|
||||
handle_view_request(bot.clone(), &q, &request_id, &db).await?;
|
||||
}
|
||||
CallbackData::ShowServerConfigs(encoded_server_name) => {
|
||||
handle_show_server_configs(bot.clone(), &q, &encoded_server_name, &db).await?;
|
||||
}
|
||||
CallbackData::SelectServerAccess(request_id) => {
|
||||
// The request_id is now the full UUID from the mapping
|
||||
let short_id = types::generate_short_request_id(&request_id);
|
||||
handle_select_server_access(bot.clone(), &q, &short_id, &db).await?;
|
||||
}
|
||||
CallbackData::ToggleServer(request_id, server_id) => {
|
||||
// Both IDs are now full UUIDs from the mapping
|
||||
let short_request_id = types::generate_short_request_id(&request_id);
|
||||
let short_server_id = types::generate_short_server_id(&server_id);
|
||||
handle_toggle_server(bot.clone(), &q, &short_request_id, &short_server_id, &db).await?;
|
||||
}
|
||||
CallbackData::ApplyServerAccess(request_id) => {
|
||||
// The request_id is now the full UUID from the mapping
|
||||
let short_id = types::generate_short_request_id(&request_id);
|
||||
handle_apply_server_access(bot.clone(), &q, &short_id, &db).await?;
|
||||
}
|
||||
CallbackData::Back => {
|
||||
// Back to main menu - edit the existing message
|
||||
handle_start_edit(bot.clone(), &q, &db).await?;
|
||||
}
|
||||
CallbackData::BackToConfigs => {
|
||||
handle_my_configs_edit(bot.clone(), &q, &db).await?;
|
||||
}
|
||||
CallbackData::BackToRequests => {
|
||||
handle_admin_requests_edit(bot.clone(), &q, &db).await?;
|
||||
}
|
||||
CallbackData::ManageUsers => {
|
||||
handle_manage_users(bot.clone(), &q, &db).await?;
|
||||
}
|
||||
CallbackData::UserList(page) => {
|
||||
handle_user_list(bot.clone(), &q, &db, page).await?;
|
||||
}
|
||||
CallbackData::UserDetails(user_id) => {
|
||||
handle_user_details(bot.clone(), &q, &db, &user_id).await?;
|
||||
}
|
||||
CallbackData::UserManageAccess(user_id) => {
|
||||
handle_user_manage_access(bot.clone(), &q, &db, &user_id).await?;
|
||||
}
|
||||
CallbackData::UserToggleServer(user_id, server_id) => {
|
||||
handle_user_toggle_server(bot.clone(), &q, &db, &user_id, &server_id).await?;
|
||||
}
|
||||
CallbackData::UserApplyAccess(user_id) => {
|
||||
handle_user_apply_access(bot.clone(), &q, &db, &user_id).await?;
|
||||
}
|
||||
CallbackData::BackToUsers(page) => {
|
||||
handle_user_list(bot.clone(), &q, &db, page).await?;
|
||||
}
|
||||
CallbackData::BackToMenu => {
|
||||
handle_start_edit(bot.clone(), &q, &db).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Unknown callback data: {}", data);
|
||||
return Err("Invalid callback data".into());
|
||||
}
|
||||
}
|
||||
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
|
||||
}.await;
|
||||
|
||||
// If any error occurred, send main menu and answer callback query
|
||||
if let Err(e) = result {
|
||||
tracing::warn!("Error handling callback query '{}': {}", q.data.as_deref().unwrap_or("None"), e);
|
||||
|
||||
// Answer the callback query first to remove loading state
|
||||
let _ = bot.answer_callback_query(q.id.clone()).await;
|
||||
|
||||
// Try to send main menu
|
||||
if let Some(message) = q.message {
|
||||
let chat_id = message.chat().id;
|
||||
let from = &q.from;
|
||||
let telegram_id = from.id.0 as i64;
|
||||
let user_repo = crate::database::repository::UserRepository::new(db.connection());
|
||||
|
||||
// Try to send main menu - if this fails too, just log it
|
||||
if let Err(menu_error) = handle_start(bot, chat_id, telegram_id, from, &user_repo, &db).await {
|
||||
tracing::error!("Failed to send main menu after callback error: {}", menu_error);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Unknown callback data: {}", data);
|
||||
bot.answer_callback_query(q.id.clone()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use teloxide::utils::command::BotCommands;
|
||||
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup, User};
|
||||
use teloxide::utils::command::BotCommands;
|
||||
|
||||
use super::super::localization::{LocalizationService, Language};
|
||||
use super::super::localization::{Language, LocalizationService};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
@@ -27,16 +27,25 @@ pub enum CallbackData {
|
||||
SubscriptionLink,
|
||||
Support,
|
||||
AdminRequests,
|
||||
ApproveRequest(String), // request_id
|
||||
DeclineRequest(String), // request_id
|
||||
ViewRequest(String), // request_id
|
||||
RequestList(u32), // page number
|
||||
ApproveRequest(String), // request_id
|
||||
DeclineRequest(String), // request_id
|
||||
ViewRequest(String), // request_id
|
||||
ShowServerConfigs(String), // server_name encoded
|
||||
Back,
|
||||
BackToConfigs, // Back to configs list from server view
|
||||
BackToRequests, // Back to requests list from request view
|
||||
SelectServerAccess(String), // request_id - show server selection after approval
|
||||
BackToConfigs, // Back to configs list from server view
|
||||
BackToRequests, // Back to requests list from request view
|
||||
SelectServerAccess(String), // request_id - show server selection after approval
|
||||
ToggleServer(String, String), // request_id, server_id - toggle server selection
|
||||
ApplyServerAccess(String), // request_id - apply selected servers
|
||||
ApplyServerAccess(String), // request_id - apply selected servers
|
||||
ManageUsers,
|
||||
UserList(u32), // page number
|
||||
UserDetails(String), // user_id
|
||||
UserManageAccess(String), // user_id
|
||||
UserToggleServer(String, String), // user_id, server_id
|
||||
UserApplyAccess(String), // user_id
|
||||
BackToUsers(u32), // page number
|
||||
BackToMenu,
|
||||
}
|
||||
|
||||
impl CallbackData {
|
||||
@@ -47,9 +56,11 @@ impl CallbackData {
|
||||
"subscription_link" => Some(CallbackData::SubscriptionLink),
|
||||
"support" => Some(CallbackData::Support),
|
||||
"admin_requests" => Some(CallbackData::AdminRequests),
|
||||
"manage_users" => Some(CallbackData::ManageUsers),
|
||||
"back" => Some(CallbackData::Back),
|
||||
"back_to_configs" => Some(CallbackData::BackToConfigs),
|
||||
"back_to_requests" => Some(CallbackData::BackToRequests),
|
||||
"back_to_menu" => Some(CallbackData::BackToMenu),
|
||||
_ => {
|
||||
if let Some(id) = data.strip_prefix("approve:") {
|
||||
Some(CallbackData::ApproveRequest(id.to_string()))
|
||||
@@ -64,7 +75,9 @@ impl CallbackData {
|
||||
} else if let Some(rest) = data.strip_prefix("t:") {
|
||||
let parts: Vec<&str> = rest.split(':').collect();
|
||||
if parts.len() == 2 {
|
||||
if let (Some(request_id), Some(server_id)) = (get_full_request_id(parts[0]), get_full_server_id(parts[1])) {
|
||||
if let (Some(request_id), Some(server_id)) =
|
||||
(get_full_request_id(parts[0]), get_full_server_id(parts[1]))
|
||||
{
|
||||
Some(CallbackData::ToggleServer(request_id, server_id))
|
||||
} else {
|
||||
None
|
||||
@@ -74,6 +87,31 @@ impl CallbackData {
|
||||
}
|
||||
} else if let Some(short_id) = data.strip_prefix("a:") {
|
||||
get_full_request_id(short_id).map(CallbackData::ApplyServerAccess)
|
||||
} else if let Some(page_str) = data.strip_prefix("request_list:") {
|
||||
page_str.parse::<u32>().ok().map(CallbackData::RequestList)
|
||||
} else if let Some(page_str) = data.strip_prefix("user_list:") {
|
||||
page_str.parse::<u32>().ok().map(CallbackData::UserList)
|
||||
} else if let Some(short_user_id) = data.strip_prefix("user_details:") {
|
||||
get_full_user_id(short_user_id).map(CallbackData::UserDetails)
|
||||
} else if let Some(short_user_id) = data.strip_prefix("user_manage:") {
|
||||
get_full_user_id(short_user_id).map(CallbackData::UserManageAccess)
|
||||
} else if let Some(rest) = data.strip_prefix("user_toggle:") {
|
||||
let parts: Vec<&str> = rest.split(':').collect();
|
||||
if parts.len() == 2 {
|
||||
if let (Some(user_id), Some(server_id)) =
|
||||
(get_full_user_id(parts[0]), get_full_server_id(parts[1]))
|
||||
{
|
||||
Some(CallbackData::UserToggleServer(user_id, server_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if let Some(short_user_id) = data.strip_prefix("user_apply:") {
|
||||
get_full_user_id(short_user_id).map(CallbackData::UserApplyAccess)
|
||||
} else if let Some(page_str) = data.strip_prefix("back_users:") {
|
||||
page_str.parse::<u32>().ok().map(CallbackData::BackToUsers)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -93,6 +131,10 @@ static REQUEST_COUNTER: OnceLock<Arc<Mutex<u32>>> = OnceLock::new();
|
||||
static SERVER_ID_MAP: OnceLock<Arc<Mutex<HashMap<String, String>>>> = OnceLock::new();
|
||||
static SERVER_COUNTER: OnceLock<Arc<Mutex<u32>>> = OnceLock::new();
|
||||
|
||||
// Global storage for user ID mappings (short ID -> full UUID)
|
||||
static USER_ID_MAP: OnceLock<Arc<Mutex<HashMap<String, String>>>> = OnceLock::new();
|
||||
static USER_COUNTER: OnceLock<Arc<Mutex<u32>>> = OnceLock::new();
|
||||
|
||||
pub fn get_selected_servers() -> &'static Arc<Mutex<HashMap<String, Vec<String>>>> {
|
||||
SELECTED_SERVERS.get_or_init(|| Arc::new(Mutex::new(HashMap::new())))
|
||||
}
|
||||
@@ -113,23 +155,31 @@ pub fn get_server_counter() -> &'static Arc<Mutex<u32>> {
|
||||
SERVER_COUNTER.get_or_init(|| Arc::new(Mutex::new(0)))
|
||||
}
|
||||
|
||||
pub fn get_user_id_map() -> &'static Arc<Mutex<HashMap<String, String>>> {
|
||||
USER_ID_MAP.get_or_init(|| Arc::new(Mutex::new(HashMap::new())))
|
||||
}
|
||||
|
||||
pub fn get_user_counter() -> &'static Arc<Mutex<u32>> {
|
||||
USER_COUNTER.get_or_init(|| Arc::new(Mutex::new(0)))
|
||||
}
|
||||
|
||||
/// Generate a short ID for a request UUID and store the mapping
|
||||
pub fn generate_short_request_id(request_uuid: &str) -> String {
|
||||
let mut counter = get_request_counter().lock().unwrap();
|
||||
let mut map = get_request_id_map().lock().unwrap();
|
||||
|
||||
|
||||
// Check if we already have a short ID for this UUID
|
||||
for (short_id, uuid) in map.iter() {
|
||||
if uuid == request_uuid {
|
||||
return short_id.clone();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate new short ID
|
||||
*counter += 1;
|
||||
let short_id = format!("r{}", counter);
|
||||
map.insert(short_id.clone(), request_uuid.to_string());
|
||||
|
||||
|
||||
short_id
|
||||
}
|
||||
|
||||
@@ -143,19 +193,19 @@ pub fn get_full_request_id(short_id: &str) -> Option<String> {
|
||||
pub fn generate_short_server_id(server_uuid: &str) -> String {
|
||||
let mut counter = get_server_counter().lock().unwrap();
|
||||
let mut map = get_server_id_map().lock().unwrap();
|
||||
|
||||
|
||||
// Check if we already have a short ID for this UUID
|
||||
for (short_id, uuid) in map.iter() {
|
||||
if uuid == server_uuid {
|
||||
return short_id.clone();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate new short ID
|
||||
*counter += 1;
|
||||
let short_id = format!("s{}", counter);
|
||||
map.insert(short_id.clone(), server_uuid.to_string());
|
||||
|
||||
|
||||
short_id
|
||||
}
|
||||
|
||||
@@ -165,6 +215,32 @@ pub fn get_full_server_id(short_id: &str) -> Option<String> {
|
||||
map.get(short_id).cloned()
|
||||
}
|
||||
|
||||
/// Generate a short ID for a user UUID and store the mapping
|
||||
pub fn generate_short_user_id(user_uuid: &str) -> String {
|
||||
let mut counter = get_user_counter().lock().unwrap();
|
||||
let mut map = get_user_id_map().lock().unwrap();
|
||||
|
||||
// Check if we already have a short ID for this UUID
|
||||
for (short_id, uuid) in map.iter() {
|
||||
if uuid == user_uuid {
|
||||
return short_id.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new short ID
|
||||
*counter += 1;
|
||||
let short_id = format!("u{}", counter);
|
||||
map.insert(short_id.clone(), user_uuid.to_string());
|
||||
|
||||
short_id
|
||||
}
|
||||
|
||||
/// Get full user UUID from short ID
|
||||
pub fn get_full_user_id(short_id: &str) -> Option<String> {
|
||||
let map = get_user_id_map().lock().unwrap();
|
||||
map.get(short_id).cloned()
|
||||
}
|
||||
|
||||
/// Helper function to get user language from Telegram user data
|
||||
pub fn get_user_language(user: &User) -> Language {
|
||||
Language::from_telegram_code(user.language_code.as_deref())
|
||||
@@ -173,27 +249,44 @@ pub fn get_user_language(user: &User) -> Language {
|
||||
/// Main keyboard for registered users
|
||||
pub fn get_main_keyboard(is_admin: bool, lang: Language) -> InlineKeyboardMarkup {
|
||||
let l10n = LocalizationService::new();
|
||||
|
||||
|
||||
let mut keyboard = vec![
|
||||
vec![InlineKeyboardButton::callback("🔗 Subscription Link", "subscription_link")],
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang.clone(), "my_configs"), "my_configs")],
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang.clone(), "support"), "support")],
|
||||
vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang.clone(), "subscription_link"),
|
||||
"subscription_link",
|
||||
)],
|
||||
vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang.clone(), "my_configs"),
|
||||
"my_configs",
|
||||
)],
|
||||
vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang.clone(), "support"),
|
||||
"support",
|
||||
)],
|
||||
];
|
||||
|
||||
|
||||
if is_admin {
|
||||
keyboard.push(vec![InlineKeyboardButton::callback(l10n.get(lang, "user_requests"), "admin_requests")]);
|
||||
keyboard.push(vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang.clone(), "user_requests"),
|
||||
"admin_requests",
|
||||
)]);
|
||||
keyboard.push(vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang, "manage_users"),
|
||||
"manage_users",
|
||||
)]);
|
||||
}
|
||||
|
||||
|
||||
InlineKeyboardMarkup::new(keyboard)
|
||||
}
|
||||
|
||||
/// Keyboard for new users
|
||||
pub fn get_new_user_keyboard(lang: Language) -> InlineKeyboardMarkup {
|
||||
let l10n = LocalizationService::new();
|
||||
|
||||
InlineKeyboardMarkup::new(vec![
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang, "get_vpn_access"), "request_access")],
|
||||
])
|
||||
|
||||
InlineKeyboardMarkup::new(vec![vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang, "get_vpn_access"),
|
||||
"request_access",
|
||||
)]])
|
||||
}
|
||||
|
||||
/// Restore UUID from compact format (without dashes)
|
||||
@@ -201,7 +294,7 @@ fn restore_uuid(compact: &str) -> Option<String> {
|
||||
if compact.len() != 32 {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
// Insert dashes at proper positions for UUID format
|
||||
let uuid_str = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
@@ -211,6 +304,6 @@ fn restore_uuid(compact: &str) -> Option<String> {
|
||||
&compact[16..20],
|
||||
&compact[20..32]
|
||||
);
|
||||
|
||||
|
||||
Some(uuid_str)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup}};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use teloxide::{
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||
};
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::database::repository::{UserRepository, UserRequestRepository};
|
||||
use super::super::localization::{Language, LocalizationService};
|
||||
use super::types::{get_main_keyboard, get_new_user_keyboard, get_user_language};
|
||||
use crate::database::entities::user_request::{CreateUserRequestDto, RequestStatus};
|
||||
use super::super::localization::{LocalizationService, Language};
|
||||
use super::types::{get_user_language, get_main_keyboard, get_new_user_keyboard};
|
||||
use crate::database::repository::{UserRepository, UserRequestRepository};
|
||||
use crate::database::DatabaseManager;
|
||||
|
||||
/// Handle start command and main menu
|
||||
pub async fn handle_start(
|
||||
@@ -28,23 +31,24 @@ pub async fn handle_start_edit(
|
||||
let from = &q.from;
|
||||
let telegram_id = from.id.0 as i64;
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
|
||||
|
||||
if let Some(msg) = &q.message {
|
||||
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
|
||||
let chat_id = regular_msg.chat.id;
|
||||
handle_start_impl(
|
||||
bot.clone(),
|
||||
chat_id,
|
||||
telegram_id,
|
||||
from,
|
||||
&user_repo,
|
||||
db,
|
||||
bot.clone(),
|
||||
chat_id,
|
||||
telegram_id,
|
||||
from,
|
||||
&user_repo,
|
||||
db,
|
||||
Some(regular_msg.id),
|
||||
Some(q.id.clone())
|
||||
).await?;
|
||||
Some(q.id.clone()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -61,37 +65,53 @@ async fn handle_start_impl(
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let lang = get_user_language(from);
|
||||
let l10n = LocalizationService::new();
|
||||
|
||||
|
||||
// Check if user exists in our database
|
||||
match user_repo.get_by_telegram_id(telegram_id).await {
|
||||
Ok(Some(user)) => {
|
||||
// Check if user is admin
|
||||
let is_admin = user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false);
|
||||
|
||||
let is_admin = user_repo
|
||||
.is_telegram_id_admin(telegram_id)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
// Check if user has any pending requests
|
||||
let request_repo = UserRequestRepository::new(db.connection().clone());
|
||||
|
||||
|
||||
// Check for existing requests
|
||||
if let Ok(existing_requests) = request_repo.find_by_telegram_id(telegram_id).await {
|
||||
if let Some(latest_request) = existing_requests.into_iter()
|
||||
.filter(|r| r.status == "pending" || r.status == "approved" || r.status == "declined")
|
||||
.max_by_key(|r| r.created_at) {
|
||||
|
||||
if let Some(latest_request) = existing_requests
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
r.status == "pending" || r.status == "approved" || r.status == "declined"
|
||||
})
|
||||
.max_by_key(|r| r.created_at)
|
||||
{
|
||||
match latest_request.status.as_str() {
|
||||
"pending" => {
|
||||
let message = l10n.format(lang.clone(), "request_pending", &[
|
||||
("status", "⏳ pending"),
|
||||
("date", &latest_request.created_at.format("%Y-%m-%d %H:%M UTC").to_string())
|
||||
]);
|
||||
|
||||
let message = l10n.format(
|
||||
lang.clone(),
|
||||
"request_pending",
|
||||
&[
|
||||
("status", "⏳ pending"),
|
||||
(
|
||||
"date",
|
||||
&latest_request
|
||||
.created_at
|
||||
.format("%Y-%m-%d %H:%M UTC")
|
||||
.to_string(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let keyboard = get_new_user_keyboard(lang);
|
||||
|
||||
|
||||
if let Some(msg_id) = edit_message_id {
|
||||
bot.edit_message_text(chat_id, msg_id, message)
|
||||
.parse_mode(teloxide::types::ParseMode::Html)
|
||||
.reply_markup(keyboard)
|
||||
.await?;
|
||||
|
||||
|
||||
if let Some(cb_id) = callback_query_id {
|
||||
bot.answer_callback_query(cb_id).await?;
|
||||
}
|
||||
@@ -104,19 +124,29 @@ async fn handle_start_impl(
|
||||
return Ok(());
|
||||
}
|
||||
"declined" => {
|
||||
let message = l10n.format(lang.clone(), "request_pending", &[
|
||||
("status", &l10n.get(lang.clone(), "request_declined_status")),
|
||||
("date", &latest_request.created_at.format("%Y-%m-%d %H:%M UTC").to_string())
|
||||
]);
|
||||
|
||||
let message = l10n.format(
|
||||
lang.clone(),
|
||||
"request_pending",
|
||||
&[
|
||||
("status", &l10n.get(lang.clone(), "request_declined_status")),
|
||||
(
|
||||
"date",
|
||||
&latest_request
|
||||
.created_at
|
||||
.format("%Y-%m-%d %H:%M UTC")
|
||||
.to_string(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let keyboard = get_new_user_keyboard(lang);
|
||||
|
||||
|
||||
if let Some(msg_id) = edit_message_id {
|
||||
bot.edit_message_text(chat_id, msg_id, message)
|
||||
.parse_mode(teloxide::types::ParseMode::Html)
|
||||
.reply_markup(keyboard)
|
||||
.await?;
|
||||
|
||||
|
||||
if let Some(cb_id) = callback_query_id {
|
||||
bot.answer_callback_query(cb_id).await?;
|
||||
}
|
||||
@@ -132,16 +162,16 @@ async fn handle_start_impl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Existing user - show main menu
|
||||
let message = l10n.format(lang.clone(), "welcome_back", &[("name", &user.name)]);
|
||||
let keyboard = get_main_keyboard(is_admin, lang);
|
||||
|
||||
|
||||
if let Some(msg_id) = edit_message_id {
|
||||
bot.edit_message_text(chat_id, msg_id, message)
|
||||
.reply_markup(keyboard)
|
||||
.await?;
|
||||
|
||||
|
||||
if let Some(cb_id) = callback_query_id {
|
||||
bot.answer_callback_query(cb_id).await?;
|
||||
}
|
||||
@@ -156,12 +186,12 @@ async fn handle_start_impl(
|
||||
let username = from.username.as_deref().unwrap_or("Unknown");
|
||||
let message = l10n.format(lang.clone(), "welcome_new_user", &[("username", username)]);
|
||||
let keyboard = get_new_user_keyboard(lang);
|
||||
|
||||
|
||||
if let Some(msg_id) = edit_message_id {
|
||||
bot.edit_message_text(chat_id, msg_id, message)
|
||||
.reply_markup(keyboard)
|
||||
.await?;
|
||||
|
||||
|
||||
if let Some(cb_id) = callback_query_id {
|
||||
bot.answer_callback_query(cb_id).await?;
|
||||
}
|
||||
@@ -176,7 +206,7 @@ async fn handle_start_impl(
|
||||
bot.send_message(chat_id, "Database error occurred").await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -190,56 +220,73 @@ pub async fn handle_request_access(
|
||||
let lang = get_user_language(from);
|
||||
let l10n = LocalizationService::new();
|
||||
let telegram_id = from.id.0 as i64;
|
||||
let chat_id = q.message.as_ref().and_then(|m| {
|
||||
match m {
|
||||
let chat_id = q
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|m| match m {
|
||||
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
|
||||
_ => None,
|
||||
}
|
||||
}).ok_or("No chat ID")?;
|
||||
|
||||
})
|
||||
.ok_or("No chat ID")?;
|
||||
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
let request_repo = UserRequestRepository::new(db.connection().clone());
|
||||
|
||||
|
||||
// Check if user already exists
|
||||
if let Some(_) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
if let Some(_) = user_repo
|
||||
.get_by_telegram_id(telegram_id)
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
{
|
||||
bot.answer_callback_query(q.id.clone())
|
||||
.text(l10n.get(lang, "already_approved"))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
// Check for existing requests
|
||||
if let Ok(existing_requests) = request_repo.find_by_telegram_id(telegram_id).await {
|
||||
if let Some(latest_request) = existing_requests.iter()
|
||||
if let Some(latest_request) = existing_requests
|
||||
.iter()
|
||||
.filter(|r| r.status == "pending")
|
||||
.max_by_key(|r| r.created_at) {
|
||||
|
||||
.max_by_key(|r| r.created_at)
|
||||
{
|
||||
// Show pending status in the message instead of just an alert
|
||||
let message = l10n.format(lang.clone(), "request_pending", &[
|
||||
("status", "⏳ pending"),
|
||||
("date", &latest_request.created_at.format("%Y-%m-%d %H:%M UTC").to_string())
|
||||
]);
|
||||
|
||||
let message = l10n.format(
|
||||
lang.clone(),
|
||||
"request_pending",
|
||||
&[
|
||||
("status", "⏳ pending"),
|
||||
(
|
||||
"date",
|
||||
&latest_request
|
||||
.created_at
|
||||
.format("%Y-%m-%d %H:%M UTC")
|
||||
.to_string(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if let Some(message_ref) = &q.message {
|
||||
if let teloxide::types::MaybeInaccessibleMessage::Regular(msg) = message_ref {
|
||||
let _ = bot.edit_message_text(chat_id, msg.id, message)
|
||||
let _ = bot
|
||||
.edit_message_text(chat_id, msg.id, message)
|
||||
.parse_mode(teloxide::types::ParseMode::Html)
|
||||
.reply_markup(InlineKeyboardMarkup::new(vec![
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
|
||||
]))
|
||||
.reply_markup(InlineKeyboardMarkup::new(vec![vec![
|
||||
InlineKeyboardButton::callback(l10n.get(lang, "back"), "back"),
|
||||
]]))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bot.answer_callback_query(q.id.clone()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
// Check for declined requests - allow new request after decline
|
||||
let _has_declined = existing_requests.iter()
|
||||
.any(|r| r.status == "declined");
|
||||
let _has_declined = existing_requests.iter().any(|r| r.status == "declined");
|
||||
}
|
||||
|
||||
|
||||
// Create new access request
|
||||
let dto = CreateUserRequestDto {
|
||||
telegram_id,
|
||||
@@ -249,23 +296,28 @@ pub async fn handle_request_access(
|
||||
request_message: Some("Access request via Telegram bot".to_string()),
|
||||
language: lang.code().to_string(),
|
||||
};
|
||||
|
||||
|
||||
match request_repo.create(dto).await {
|
||||
Ok(request) => {
|
||||
// Edit message to show success
|
||||
if let Some(message) = &q.message {
|
||||
if let teloxide::types::MaybeInaccessibleMessage::Regular(msg) = message {
|
||||
let _ = bot.edit_message_text(chat_id, msg.id, l10n.get(lang.clone(), "request_submitted"))
|
||||
.reply_markup(InlineKeyboardMarkup::new(vec![
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
|
||||
]))
|
||||
let _ = bot
|
||||
.edit_message_text(
|
||||
chat_id,
|
||||
msg.id,
|
||||
l10n.get(lang.clone(), "request_submitted"),
|
||||
)
|
||||
.reply_markup(InlineKeyboardMarkup::new(vec![vec![
|
||||
InlineKeyboardButton::callback(l10n.get(lang, "back"), "back"),
|
||||
]]))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Notify admins
|
||||
notify_admins_new_request(&bot, &request, db).await?;
|
||||
|
||||
|
||||
bot.answer_callback_query(q.id.clone()).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -275,7 +327,7 @@ pub async fn handle_request_access(
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -289,64 +341,83 @@ pub async fn handle_my_configs_edit(
|
||||
let lang = get_user_language(from);
|
||||
let l10n = LocalizationService::new();
|
||||
let telegram_id = from.id.0 as i64;
|
||||
let chat_id = q.message.as_ref().and_then(|m| {
|
||||
match m {
|
||||
let chat_id = q
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|m| match m {
|
||||
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
|
||||
_ => None,
|
||||
}
|
||||
}).ok_or("No chat ID")?;
|
||||
|
||||
})
|
||||
.ok_or("No chat ID")?;
|
||||
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
let inbound_users_repo = crate::database::repository::InboundUsersRepository::new(db.connection().clone());
|
||||
let inbound_users_repo =
|
||||
crate::database::repository::InboundUsersRepository::new(db.connection().clone());
|
||||
let uri_service = crate::services::UriGeneratorService::new();
|
||||
|
||||
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
|
||||
if let Some(user) = user_repo
|
||||
.get_by_telegram_id(telegram_id)
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
{
|
||||
// Get all active inbound users for this user
|
||||
let inbound_users = inbound_users_repo.find_by_user_id(user.id).await.unwrap_or_default();
|
||||
|
||||
let inbound_users = inbound_users_repo
|
||||
.find_by_user_id(user.id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if inbound_users.is_empty() {
|
||||
// Edit message to show no configs available
|
||||
if let Some(msg) = &q.message {
|
||||
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
|
||||
bot.edit_message_text(chat_id, regular_msg.id, l10n.get(lang.clone(), "no_configs_available"))
|
||||
.reply_markup(InlineKeyboardMarkup::new(vec![
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
|
||||
]))
|
||||
.await?;
|
||||
bot.edit_message_text(
|
||||
chat_id,
|
||||
regular_msg.id,
|
||||
l10n.get(lang.clone(), "no_configs_available"),
|
||||
)
|
||||
.reply_markup(InlineKeyboardMarkup::new(vec![vec![
|
||||
InlineKeyboardButton::callback(l10n.get(lang, "back"), "back"),
|
||||
]]))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
bot.answer_callback_query(q.id.clone()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
// Structure to hold config with inbound_id
|
||||
#[derive(Debug, Clone)]
|
||||
struct ConfigWithInbound {
|
||||
client_config: crate::services::uri_generator::ClientConfig,
|
||||
server_inbound_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
|
||||
// Group configurations by server name
|
||||
let mut servers: std::collections::HashMap<String, Vec<ConfigWithInbound>> = std::collections::HashMap::new();
|
||||
|
||||
let mut servers: std::collections::HashMap<String, Vec<ConfigWithInbound>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for inbound_user in inbound_users {
|
||||
if !inbound_user.is_active {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Get client config data for this specific inbound
|
||||
if let Ok(Some(config_data)) = inbound_users_repo.get_client_config_data(user.id, inbound_user.server_inbound_id).await {
|
||||
if let Ok(Some(config_data)) = inbound_users_repo
|
||||
.get_client_config_data(user.id, inbound_user.server_inbound_id)
|
||||
.await
|
||||
{
|
||||
match uri_service.generate_client_config(user.id, &config_data) {
|
||||
Ok(client_config) => {
|
||||
let config_with_inbound = ConfigWithInbound {
|
||||
client_config: client_config.clone(),
|
||||
server_inbound_id: inbound_user.server_inbound_id,
|
||||
};
|
||||
|
||||
servers.entry(client_config.server_name.clone())
|
||||
|
||||
servers
|
||||
.entry(client_config.server_name.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(config_with_inbound);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to generate client config: {}", e);
|
||||
continue;
|
||||
@@ -354,14 +425,14 @@ pub async fn handle_my_configs_edit(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Build message with statistics only
|
||||
let mut message_lines = vec![l10n.get(lang.clone(), "your_configurations")];
|
||||
|
||||
|
||||
// Calculate statistics
|
||||
let server_count = servers.len();
|
||||
let total_configs = servers.values().map(|configs| configs.len()).sum::<usize>();
|
||||
|
||||
|
||||
// Count unique protocols
|
||||
let mut protocols = std::collections::HashSet::new();
|
||||
for configs in servers.values() {
|
||||
@@ -369,96 +440,122 @@ pub async fn handle_my_configs_edit(
|
||||
protocols.insert(config_with_inbound.client_config.protocol.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let server_word = match lang {
|
||||
Language::Russian => {
|
||||
if server_count == 1 { "сервер" }
|
||||
else if server_count < 5 { "сервера" }
|
||||
else { "серверов" }
|
||||
},
|
||||
if server_count == 1 {
|
||||
"сервер"
|
||||
} else if server_count < 5 {
|
||||
"сервера"
|
||||
} else {
|
||||
"серверов"
|
||||
}
|
||||
}
|
||||
Language::English => {
|
||||
if server_count == 1 { "server" }
|
||||
else { "servers" }
|
||||
if server_count == 1 {
|
||||
"server"
|
||||
} else {
|
||||
"servers"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let config_word = match lang {
|
||||
Language::Russian => {
|
||||
if total_configs == 1 { "конфигурация" }
|
||||
else if total_configs < 5 { "конфигурации" }
|
||||
else { "конфигураций" }
|
||||
},
|
||||
if total_configs == 1 {
|
||||
"конфигурация"
|
||||
} else if total_configs < 5 {
|
||||
"конфигурации"
|
||||
} else {
|
||||
"конфигураций"
|
||||
}
|
||||
}
|
||||
Language::English => {
|
||||
if total_configs == 1 { "configuration" }
|
||||
else { "configurations" }
|
||||
if total_configs == 1 {
|
||||
"configuration"
|
||||
} else {
|
||||
"configurations"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let protocol_word = match lang {
|
||||
Language::Russian => {
|
||||
if protocols.len() == 1 { "протокол" }
|
||||
else if protocols.len() < 5 { "протокола" }
|
||||
else { "протоколов" }
|
||||
},
|
||||
if protocols.len() == 1 {
|
||||
"протокол"
|
||||
} else if protocols.len() < 5 {
|
||||
"протокола"
|
||||
} else {
|
||||
"протоколов"
|
||||
}
|
||||
}
|
||||
Language::English => {
|
||||
if protocols.len() == 1 { "protocol" }
|
||||
else { "protocols" }
|
||||
if protocols.len() == 1 {
|
||||
"protocol"
|
||||
} else {
|
||||
"protocols"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
message_lines.push(format!(
|
||||
"\n📊 {} {} • {} {} • {} {}",
|
||||
server_count, server_word,
|
||||
total_configs, config_word,
|
||||
protocols.len(), protocol_word
|
||||
server_count,
|
||||
server_word,
|
||||
total_configs,
|
||||
config_word,
|
||||
protocols.len(),
|
||||
protocol_word
|
||||
));
|
||||
|
||||
|
||||
// Create keyboard with buttons for each server
|
||||
let mut keyboard_buttons = vec![];
|
||||
|
||||
|
||||
for (server_name, configs) in servers.iter() {
|
||||
// Encode server name to avoid issues with special characters
|
||||
let encoded_server_name = general_purpose::STANDARD.encode(server_name.as_bytes());
|
||||
let config_count = configs.len();
|
||||
|
||||
|
||||
let config_suffix = match lang {
|
||||
Language::Russian => {
|
||||
if config_count == 1 {
|
||||
""
|
||||
} else if config_count < 5 {
|
||||
"а"
|
||||
} else {
|
||||
"ов"
|
||||
if config_count == 1 {
|
||||
""
|
||||
} else if config_count < 5 {
|
||||
"а"
|
||||
} else {
|
||||
"ов"
|
||||
}
|
||||
},
|
||||
}
|
||||
Language::English => {
|
||||
if config_count == 1 {
|
||||
""
|
||||
} else {
|
||||
"s"
|
||||
if config_count == 1 {
|
||||
""
|
||||
} else {
|
||||
"s"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let config_word = match lang {
|
||||
Language::Russian => "конфиг",
|
||||
Language::English => "config",
|
||||
};
|
||||
|
||||
keyboard_buttons.push(vec![
|
||||
InlineKeyboardButton::callback(
|
||||
format!("🖥️ {} ({} {}{})", server_name, config_count, config_word, config_suffix),
|
||||
format!("server_configs:{}", encoded_server_name)
|
||||
)
|
||||
]);
|
||||
|
||||
keyboard_buttons.push(vec![InlineKeyboardButton::callback(
|
||||
format!(
|
||||
"🖥️ {} ({} {}{})",
|
||||
server_name, config_count, config_word, config_suffix
|
||||
),
|
||||
format!("server_configs:{}", encoded_server_name),
|
||||
)]);
|
||||
}
|
||||
|
||||
keyboard_buttons.push(vec![
|
||||
InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")
|
||||
]);
|
||||
|
||||
|
||||
keyboard_buttons.push(vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang, "back"),
|
||||
"back",
|
||||
)]);
|
||||
|
||||
let message = message_lines.join("\n");
|
||||
|
||||
|
||||
// Edit the existing message instead of sending a new one
|
||||
if let Some(msg) = &q.message {
|
||||
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
|
||||
@@ -468,10 +565,10 @@ pub async fn handle_my_configs_edit(
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bot.answer_callback_query(q.id.clone()).await?;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -486,42 +583,55 @@ pub async fn handle_show_server_configs(
|
||||
let lang = get_user_language(from);
|
||||
let l10n = LocalizationService::new();
|
||||
let telegram_id = from.id.0 as i64;
|
||||
let chat_id = q.message.as_ref().and_then(|m| {
|
||||
match m {
|
||||
let chat_id = q
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|m| match m {
|
||||
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
|
||||
_ => None,
|
||||
}
|
||||
}).ok_or("No chat ID")?;
|
||||
|
||||
})
|
||||
.ok_or("No chat ID")?;
|
||||
|
||||
// Decode server name
|
||||
let server_name = match general_purpose::STANDARD.decode(encoded_server_name) {
|
||||
Ok(bytes) => String::from_utf8(bytes).map_err(|_| "Invalid server name encoding")?,
|
||||
Err(_) => return Ok(()), // Invalid encoding, ignore
|
||||
};
|
||||
|
||||
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
let inbound_users_repo = crate::database::repository::InboundUsersRepository::new(db.connection().clone());
|
||||
let inbound_users_repo =
|
||||
crate::database::repository::InboundUsersRepository::new(db.connection().clone());
|
||||
let uri_service = crate::services::UriGeneratorService::new();
|
||||
|
||||
|
||||
// Get user from telegram_id
|
||||
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
if let Some(user) = user_repo
|
||||
.get_by_telegram_id(telegram_id)
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
{
|
||||
// Get all active inbound users for this user
|
||||
let inbound_users = inbound_users_repo.find_by_user_id(user.id).await.unwrap_or_default();
|
||||
|
||||
let inbound_users = inbound_users_repo
|
||||
.find_by_user_id(user.id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut server_configs = Vec::new();
|
||||
|
||||
|
||||
for inbound_user in inbound_users {
|
||||
if !inbound_user.is_active {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Get client config data for this specific inbound
|
||||
if let Ok(Some(config_data)) = inbound_users_repo.get_client_config_data(user.id, inbound_user.server_inbound_id).await {
|
||||
if let Ok(Some(config_data)) = inbound_users_repo
|
||||
.get_client_config_data(user.id, inbound_user.server_inbound_id)
|
||||
.await
|
||||
{
|
||||
if config_data.server_name == server_name {
|
||||
match uri_service.generate_client_config(user.id, &config_data) {
|
||||
Ok(client_config) => {
|
||||
server_configs.push(client_config);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to generate client config: {}", e);
|
||||
continue;
|
||||
@@ -530,28 +640,30 @@ pub async fn handle_show_server_configs(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if server_configs.is_empty() {
|
||||
bot.answer_callback_query(q.id.clone())
|
||||
.text(l10n.get(lang, "config_not_found"))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
// Build message with all configs for this server
|
||||
let mut message_lines = vec![
|
||||
l10n.format(lang.clone(), "server_configs_title", &[("server_name", &server_name)])
|
||||
];
|
||||
|
||||
let mut message_lines = vec![l10n.format(
|
||||
lang.clone(),
|
||||
"server_configs_title",
|
||||
&[("server_name", &server_name)],
|
||||
)];
|
||||
|
||||
for config in &server_configs {
|
||||
let protocol_emoji = match config.protocol.as_str() {
|
||||
"vless" => "🔵",
|
||||
"vmess" => "🟢",
|
||||
"vmess" => "🟢",
|
||||
"trojan" => "🔴",
|
||||
"shadowsocks" => "🟡",
|
||||
_ => "⚪"
|
||||
_ => "⚪",
|
||||
};
|
||||
|
||||
|
||||
message_lines.push(format!(
|
||||
"\n{} <b>{} - {}</b> ({})",
|
||||
protocol_emoji,
|
||||
@@ -559,17 +671,18 @@ pub async fn handle_show_server_configs(
|
||||
config.template_name,
|
||||
config.protocol.to_uppercase()
|
||||
));
|
||||
|
||||
|
||||
message_lines.push(format!("<code>{}</code>", config.uri));
|
||||
}
|
||||
|
||||
|
||||
// Create back button
|
||||
let keyboard = InlineKeyboardMarkup::new(vec![
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back_to_configs")],
|
||||
]);
|
||||
|
||||
let keyboard = InlineKeyboardMarkup::new(vec![vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang, "back"),
|
||||
"back_to_configs",
|
||||
)]]);
|
||||
|
||||
let message = message_lines.join("\n");
|
||||
|
||||
|
||||
// Edit the existing message instead of sending a new one
|
||||
if let Some(msg) = &q.message {
|
||||
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
|
||||
@@ -579,14 +692,14 @@ pub async fn handle_show_server_configs(
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bot.answer_callback_query(q.id.clone()).await?;
|
||||
} else {
|
||||
bot.answer_callback_query(q.id.clone())
|
||||
.text(l10n.get(lang, "unauthorized"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -598,17 +711,20 @@ pub async fn handle_support(
|
||||
let from = &q.from;
|
||||
let lang = get_user_language(from);
|
||||
let l10n = LocalizationService::new();
|
||||
let chat_id = q.message.as_ref().and_then(|m| {
|
||||
match m {
|
||||
let chat_id = q
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|m| match m {
|
||||
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
|
||||
_ => None,
|
||||
}
|
||||
}).ok_or("No chat ID")?;
|
||||
|
||||
let keyboard = InlineKeyboardMarkup::new(vec![
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang.clone(), "back"), "back")],
|
||||
]);
|
||||
|
||||
})
|
||||
.ok_or("No chat ID")?;
|
||||
|
||||
let keyboard = InlineKeyboardMarkup::new(vec![vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang.clone(), "back"),
|
||||
"back",
|
||||
)]]);
|
||||
|
||||
// Edit the existing message instead of sending a new one
|
||||
if let Some(msg) = &q.message {
|
||||
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
|
||||
@@ -618,9 +734,9 @@ pub async fn handle_support(
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bot.answer_callback_query(q.id.clone()).await?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -631,40 +747,61 @@ async fn notify_admins_new_request(
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
|
||||
|
||||
// Get all admins
|
||||
let admins = user_repo.get_telegram_admins().await.unwrap_or_default();
|
||||
|
||||
|
||||
if !admins.is_empty() {
|
||||
let lang = Language::English; // Default admin language
|
||||
let l10n = LocalizationService::new();
|
||||
|
||||
let message = l10n.format(lang.clone(), "new_access_request", &[
|
||||
("first_name", &request.telegram_first_name.as_deref().unwrap_or("")),
|
||||
("last_name", &request.telegram_last_name.as_deref().unwrap_or("")),
|
||||
("username", &request.telegram_username.as_deref().unwrap_or("unknown")),
|
||||
]);
|
||||
|
||||
|
||||
let message = l10n.format(
|
||||
lang.clone(),
|
||||
"new_access_request",
|
||||
&[
|
||||
(
|
||||
"first_name",
|
||||
&request.telegram_first_name.as_deref().unwrap_or(""),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
&request.telegram_last_name.as_deref().unwrap_or(""),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
&request.telegram_username.as_deref().unwrap_or("unknown"),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let keyboard = InlineKeyboardMarkup::new(vec![
|
||||
vec![
|
||||
InlineKeyboardButton::callback(l10n.get(lang.clone(), "approve"), format!("approve:{}", request.id)),
|
||||
InlineKeyboardButton::callback(l10n.get(lang.clone(), "decline"), format!("decline:{}", request.id)),
|
||||
],
|
||||
vec![
|
||||
InlineKeyboardButton::callback("📋 All Requests", "back_to_requests"),
|
||||
InlineKeyboardButton::callback(
|
||||
l10n.get(lang.clone(), "approve"),
|
||||
format!("approve:{}", request.id),
|
||||
),
|
||||
InlineKeyboardButton::callback(
|
||||
l10n.get(lang.clone(), "decline"),
|
||||
format!("decline:{}", request.id),
|
||||
),
|
||||
],
|
||||
vec![InlineKeyboardButton::callback(
|
||||
"📋 All Requests",
|
||||
"back_to_requests",
|
||||
)],
|
||||
]);
|
||||
|
||||
|
||||
for admin in admins {
|
||||
if let Some(telegram_id) = admin.telegram_id {
|
||||
let _ = bot.send_message(ChatId(telegram_id), &message)
|
||||
let _ = bot
|
||||
.send_message(ChatId(telegram_id), &message)
|
||||
.parse_mode(teloxide::types::ParseMode::Html)
|
||||
.reply_markup(keyboard.clone())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -685,7 +822,7 @@ pub async fn handle_subscription_link(
|
||||
if let Ok(Some(user)) = user_repo.get_by_telegram_id(telegram_id).await {
|
||||
// Generate subscription URL
|
||||
let subscription_url = format!("{}/sub/{}", app_config.web.base_url, user.id);
|
||||
|
||||
|
||||
let message = match lang {
|
||||
Language::Russian => {
|
||||
format!(
|
||||
@@ -695,7 +832,7 @@ pub async fn handle_subscription_link(
|
||||
💡 <i>Эта ссылка содержит все ваши конфигурации и автоматически обновляется при изменениях</i>",
|
||||
subscription_url
|
||||
)
|
||||
},
|
||||
}
|
||||
Language::English => {
|
||||
format!(
|
||||
"🔗 <b>Your Subscription Link</b>\n\n\
|
||||
@@ -707,9 +844,10 @@ pub async fn handle_subscription_link(
|
||||
}
|
||||
};
|
||||
|
||||
let keyboard = InlineKeyboardMarkup::new(vec![
|
||||
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
|
||||
]);
|
||||
let keyboard = InlineKeyboardMarkup::new(vec![vec![InlineKeyboardButton::callback(
|
||||
l10n.get(lang, "back"),
|
||||
"back",
|
||||
)]]);
|
||||
|
||||
// Edit the existing message
|
||||
if let Some(msg) = &q.message {
|
||||
@@ -731,4 +869,4 @@ pub async fn handle_subscription_link(
|
||||
|
||||
bot.answer_callback_query(q.id.clone()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Language {
|
||||
@@ -37,20 +37,20 @@ pub struct Translations {
|
||||
pub back: String,
|
||||
pub approve: String,
|
||||
pub decline: String,
|
||||
|
||||
|
||||
// Request handling
|
||||
pub already_pending: String,
|
||||
pub already_approved: String,
|
||||
pub already_declined: String,
|
||||
pub request_submitted: String,
|
||||
pub request_submit_failed: String,
|
||||
|
||||
|
||||
// Approval/Decline messages
|
||||
pub request_approved: String,
|
||||
pub request_declined: String,
|
||||
pub request_approved_notification: String,
|
||||
pub request_declined_notification: String,
|
||||
|
||||
|
||||
// Admin messages
|
||||
pub new_access_request: String,
|
||||
pub no_pending_requests: String,
|
||||
@@ -59,22 +59,22 @@ pub struct Translations {
|
||||
pub request_approved_admin: String,
|
||||
pub request_declined_admin: String,
|
||||
pub user_creation_failed: String,
|
||||
|
||||
|
||||
// Support
|
||||
pub support_info: String,
|
||||
|
||||
|
||||
// Stats
|
||||
pub statistics: String,
|
||||
pub total_users: String,
|
||||
pub total_servers: String,
|
||||
pub total_inbounds: String,
|
||||
pub pending_requests: String,
|
||||
|
||||
|
||||
// Broadcast
|
||||
pub broadcast_complete: String,
|
||||
pub sent: String,
|
||||
pub failed: String,
|
||||
|
||||
|
||||
// Configs
|
||||
pub configs_coming_soon: String,
|
||||
pub your_configurations: String,
|
||||
@@ -83,7 +83,28 @@ pub struct Translations {
|
||||
pub config_copied: String,
|
||||
pub config_not_found: String,
|
||||
pub server_configs_title: String,
|
||||
|
||||
|
||||
// Subscription
|
||||
pub subscription_link: String,
|
||||
|
||||
// User Management
|
||||
pub manage_users: String,
|
||||
pub user_list: String,
|
||||
pub user_details: String,
|
||||
pub manage_access: String,
|
||||
pub remove_access: String,
|
||||
pub grant_access: String,
|
||||
pub user_info: String,
|
||||
pub no_users_found: String,
|
||||
pub page_info: String,
|
||||
pub next_page: String,
|
||||
pub prev_page: String,
|
||||
pub back_to_users: String,
|
||||
pub back_to_menu: String,
|
||||
pub access_updated: String,
|
||||
pub access_removed: String,
|
||||
pub access_granted: String,
|
||||
|
||||
// Errors
|
||||
pub error_occurred: String,
|
||||
pub admin_not_found: String,
|
||||
@@ -98,20 +119,22 @@ pub struct LocalizationService {
|
||||
impl LocalizationService {
|
||||
pub fn new() -> Self {
|
||||
let mut translations = HashMap::new();
|
||||
|
||||
|
||||
// Load English translations
|
||||
translations.insert(Language::English, Self::load_english());
|
||||
|
||||
|
||||
// Load Russian translations
|
||||
translations.insert(Language::Russian, Self::load_russian());
|
||||
|
||||
|
||||
Self { translations }
|
||||
}
|
||||
|
||||
pub fn get(&self, lang: Language, key: &str) -> String {
|
||||
let translations = self.translations.get(&lang)
|
||||
let translations = self
|
||||
.translations
|
||||
.get(&lang)
|
||||
.unwrap_or_else(|| self.translations.get(&Language::English).unwrap());
|
||||
|
||||
|
||||
match key {
|
||||
"welcome_new_user" => translations.welcome_new_user.clone(),
|
||||
"welcome_back" => translations.welcome_back.clone(),
|
||||
@@ -157,6 +180,23 @@ impl LocalizationService {
|
||||
"config_copied" => translations.config_copied.clone(),
|
||||
"config_not_found" => translations.config_not_found.clone(),
|
||||
"server_configs_title" => translations.server_configs_title.clone(),
|
||||
"subscription_link" => translations.subscription_link.clone(),
|
||||
"manage_users" => translations.manage_users.clone(),
|
||||
"user_list" => translations.user_list.clone(),
|
||||
"user_details" => translations.user_details.clone(),
|
||||
"manage_access" => translations.manage_access.clone(),
|
||||
"remove_access" => translations.remove_access.clone(),
|
||||
"grant_access" => translations.grant_access.clone(),
|
||||
"user_info" => translations.user_info.clone(),
|
||||
"no_users_found" => translations.no_users_found.clone(),
|
||||
"page_info" => translations.page_info.clone(),
|
||||
"next_page" => translations.next_page.clone(),
|
||||
"prev_page" => translations.prev_page.clone(),
|
||||
"back_to_users" => translations.back_to_users.clone(),
|
||||
"back_to_menu" => translations.back_to_menu.clone(),
|
||||
"access_updated" => translations.access_updated.clone(),
|
||||
"access_removed" => translations.access_removed.clone(),
|
||||
"access_granted" => translations.access_granted.clone(),
|
||||
"error_occurred" => translations.error_occurred.clone(),
|
||||
"admin_not_found" => translations.admin_not_found.clone(),
|
||||
"request_not_found" => translations.request_not_found.clone(),
|
||||
@@ -183,7 +223,7 @@ impl LocalizationService {
|
||||
get_vpn_access: "🚀 Get VPN Access".to_string(),
|
||||
my_configs: "📋 My Configs".to_string(),
|
||||
support: "💬 Support".to_string(),
|
||||
user_requests: "👥 User Requests".to_string(),
|
||||
user_requests: "❔ User Requests".to_string(),
|
||||
back: "🔙 Back".to_string(),
|
||||
approve: "✅ Approve".to_string(),
|
||||
decline: "❌ Decline".to_string(),
|
||||
@@ -201,13 +241,13 @@ impl LocalizationService {
|
||||
|
||||
new_access_request: "🔔 <b>New Access Request</b>\n\n👤 Name: {first_name} {last_name}\n🆔 Username: @{username}\n\nUse /requests to review".to_string(),
|
||||
no_pending_requests: "No pending access requests".to_string(),
|
||||
access_request_details: "📋 <b>Access Request</b>\n\n👤 Name: {full_name}\n🆔 Telegram: {telegram_link}\n📅 Requested: {date}\n\nMessage: {message}".to_string(),
|
||||
access_request_details: "❔ <b>Access Request</b>\n\n👤 Name: {full_name}\n🆔 Telegram: {telegram_link}\n📅 Requested: {date}\n\nMessage: {message}".to_string(),
|
||||
unauthorized: "❌ You are not authorized to use this command".to_string(),
|
||||
request_approved_admin: "✅ Request approved".to_string(),
|
||||
request_declined_admin: "❌ Request declined".to_string(),
|
||||
user_creation_failed: "❌ Failed to create user account: {error}\n\nPlease try again or contact technical support.".to_string(),
|
||||
|
||||
support_info: "💬 <b>Support Information</b>\n\nIf you need help, please contact the administrators.\n\nYou can also check the documentation at:\nhttps://github.com/OutFleet".to_string(),
|
||||
support_info: "💬 <b>Support Information</b>\n\n📱 <b>How to connect:</b>\n1. Download v2raytun app for Android or iOS from:\n https://v2raytun.com/\n\n2. Add your subscription link from \"🔗 Subscription Link\" menu\n OR\n Add individual server links from \"📋 My Configs\"\n\n3. Connect and enjoy secure VPN!\n\n❓ If you need help, please contact the administrators.".to_string(),
|
||||
|
||||
statistics: "📊 <b>Statistics</b>\n\n👥 Total Users: {users}\n🖥️ Total Servers: {servers}\n📡 Total Inbounds: {inbounds}\n⏳ Pending Requests: {pending}".to_string(),
|
||||
total_users: "👥 Total Users".to_string(),
|
||||
@@ -227,6 +267,25 @@ impl LocalizationService {
|
||||
config_not_found: "❌ Configuration not found".to_string(),
|
||||
server_configs_title: "🖥️ <b>{server_name}</b> - Connection Links".to_string(),
|
||||
|
||||
subscription_link: "🔗 Subscription Link".to_string(),
|
||||
|
||||
manage_users: "👥 Manage Users".to_string(),
|
||||
user_list: "👥 User List".to_string(),
|
||||
user_details: "👤 User Details".to_string(),
|
||||
manage_access: "🔧 Manage Access".to_string(),
|
||||
remove_access: "❌ Remove Access".to_string(),
|
||||
grant_access: "✅ Grant Access".to_string(),
|
||||
user_info: "User Information".to_string(),
|
||||
no_users_found: "No users found".to_string(),
|
||||
page_info: "Page {page} of {total}".to_string(),
|
||||
next_page: "Next →".to_string(),
|
||||
prev_page: "← Previous".to_string(),
|
||||
back_to_users: "👥 Back to Users".to_string(),
|
||||
back_to_menu: "🏠 Main Menu".to_string(),
|
||||
access_updated: "✅ Access updated successfully".to_string(),
|
||||
access_removed: "❌ Access removed successfully".to_string(),
|
||||
access_granted: "✅ Access granted successfully".to_string(),
|
||||
|
||||
error_occurred: "An error occurred".to_string(),
|
||||
admin_not_found: "Admin not found".to_string(),
|
||||
request_not_found: "Request not found".to_string(),
|
||||
@@ -244,7 +303,7 @@ impl LocalizationService {
|
||||
get_vpn_access: "🚀 Получить доступ к VPN".to_string(),
|
||||
my_configs: "📋 Мои конфигурации".to_string(),
|
||||
support: "💬 Поддержка".to_string(),
|
||||
user_requests: "👥 Запросы пользователей".to_string(),
|
||||
user_requests: "❔ Запросы пользователей".to_string(),
|
||||
back: "🔙 Назад".to_string(),
|
||||
approve: "✅ Одобрить".to_string(),
|
||||
decline: "❌ Отклонить".to_string(),
|
||||
@@ -262,13 +321,13 @@ impl LocalizationService {
|
||||
|
||||
new_access_request: "🔔 <b>Новый запрос на доступ</b>\n\n👤 Имя: {first_name} {last_name}\n🆔 Имя пользователя: @{username}\n\nИспользуйте /requests для просмотра".to_string(),
|
||||
no_pending_requests: "Нет ожидающих запросов на доступ".to_string(),
|
||||
access_request_details: "📋 <b>Запрос на доступ</b>\n\n👤 Имя: {full_name}\n🆔 Telegram: {telegram_link}\n📅 Запрошено: {date}\n\nСообщение: {message}".to_string(),
|
||||
access_request_details: "❔ <b>Запрос на доступ</b>\n\n👤 Имя: {full_name}\n🆔 Telegram: {telegram_link}\n📅 Запрошено: {date}\n\nСообщение: {message}".to_string(),
|
||||
unauthorized: "❌ У вас нет прав для использования этой команды".to_string(),
|
||||
request_approved_admin: "✅ Запрос одобрен".to_string(),
|
||||
request_declined_admin: "❌ Запрос отклонен".to_string(),
|
||||
user_creation_failed: "❌ Не удалось создать аккаунт пользователя: {error}\n\nПожалуйста, попробуйте еще раз или обратитесь в техническую поддержку.".to_string(),
|
||||
|
||||
support_info: "💬 <b>Информация о поддержке</b>\n\nЕсли вам нужна помощь, пожалуйста, свяжитесь с администраторами.\n\nВы также можете ознакомиться с документацией по адресу:\nhttps://github.com/OutFleet".to_string(),
|
||||
support_info: "💬 <b>Информация о поддержке</b>\n\n📱 <b>Как подключиться:</b>\n1. Скачайте приложение v2raytun для Android или iOS с сайта:\n https://v2raytun.com/\n\n2. Добавьте ссылку подписки из меню \"🔗 Ссылка подписки\"\n ИЛИ\n Добавьте отдельные ссылки серверов из \"📋 Мои конфигурации\"\n\n3. Подключайтесь и наслаждайтесь безопасным VPN!\n\n❓ Если нужна помощь, обратитесь к администраторам.".to_string(),
|
||||
|
||||
statistics: "📊 <b>Статистика</b>\n\n👥 Всего пользователей: {users}\n🖥️ Всего серверов: {servers}\n📡 Всего входящих подключений: {inbounds}\n⏳ Ожидающих запросов: {pending}".to_string(),
|
||||
total_users: "👥 Всего пользователей".to_string(),
|
||||
@@ -288,10 +347,29 @@ impl LocalizationService {
|
||||
config_not_found: "❌ Конфигурация не найдена".to_string(),
|
||||
server_configs_title: "🖥️ <b>{server_name}</b> - Ссылки для подключения".to_string(),
|
||||
|
||||
subscription_link: "🔗 Ссылка подписки".to_string(),
|
||||
|
||||
manage_users: "👥 Управление пользователями".to_string(),
|
||||
user_list: "👥 Список пользователей".to_string(),
|
||||
user_details: "👤 Данные пользователя".to_string(),
|
||||
manage_access: "🔧 Управление доступом".to_string(),
|
||||
remove_access: "❌ Убрать доступ".to_string(),
|
||||
grant_access: "✅ Предоставить доступ".to_string(),
|
||||
user_info: "Информация о пользователе".to_string(),
|
||||
no_users_found: "Пользователи не найдены".to_string(),
|
||||
page_info: "Страница {page} из {total}".to_string(),
|
||||
next_page: "Далее →".to_string(),
|
||||
prev_page: "← Назад".to_string(),
|
||||
back_to_users: "👥 К пользователям".to_string(),
|
||||
back_to_menu: "🏠 Главное меню".to_string(),
|
||||
access_updated: "✅ Доступ успешно обновлен".to_string(),
|
||||
access_removed: "❌ Доступ успешно убран".to_string(),
|
||||
access_granted: "✅ Доступ успешно предоставлен".to_string(),
|
||||
|
||||
error_occurred: "Произошла ошибка".to_string(),
|
||||
admin_not_found: "Администратор не найден".to_string(),
|
||||
request_not_found: "Запрос не найден".to_string(),
|
||||
invalid_request_id: "Неверный ID запроса".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use teloxide::{Bot, prelude::*};
|
||||
use teloxide::{prelude::*, Bot};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::database::repository::TelegramConfigRepository;
|
||||
use crate::database::entities::telegram_config::Model as TelegramConfig;
|
||||
use crate::config::AppConfig;
|
||||
use crate::database::entities::telegram_config::Model as TelegramConfig;
|
||||
use crate::database::repository::TelegramConfigRepository;
|
||||
use crate::database::DatabaseManager;
|
||||
|
||||
pub mod bot;
|
||||
pub mod handlers;
|
||||
pub mod error;
|
||||
pub mod handlers;
|
||||
pub mod localization;
|
||||
|
||||
pub use error::TelegramError;
|
||||
@@ -40,12 +40,12 @@ impl TelegramService {
|
||||
/// Initialize and start the bot if active configuration exists
|
||||
pub async fn initialize(&self) -> Result<()> {
|
||||
let repo = TelegramConfigRepository::new(self.db.connection());
|
||||
|
||||
|
||||
// Get active configuration
|
||||
if let Some(config) = repo.get_active().await? {
|
||||
self.start_with_config(config).await?;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ impl TelegramService {
|
||||
|
||||
// Create new bot instance
|
||||
let bot = Bot::new(&config.bot_token);
|
||||
|
||||
|
||||
// Verify token by calling getMe
|
||||
match bot.get_me().await {
|
||||
Ok(me) => {
|
||||
@@ -87,7 +87,7 @@ impl TelegramService {
|
||||
|
||||
let db = self.db.clone();
|
||||
let app_config = self.app_config.clone();
|
||||
|
||||
|
||||
// Spawn polling task
|
||||
tokio::spawn(async move {
|
||||
bot::run_polling(bot, db, app_config, rx).await;
|
||||
@@ -114,7 +114,7 @@ impl TelegramService {
|
||||
/// Update configuration and restart if needed
|
||||
pub async fn update_config(&self, config_id: Uuid) -> Result<()> {
|
||||
let repo = TelegramConfigRepository::new(self.db.connection());
|
||||
|
||||
|
||||
if let Some(config) = repo.find_by_id(config_id).await? {
|
||||
if config.is_active {
|
||||
self.start_with_config(config).await?;
|
||||
@@ -122,7 +122,7 @@ impl TelegramService {
|
||||
self.stop().await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ impl TelegramService {
|
||||
pub async fn get_status(&self) -> BotStatus {
|
||||
let bot_guard = self.bot.read().await;
|
||||
let config_guard = self.config.read().await;
|
||||
|
||||
|
||||
BotStatus {
|
||||
is_running: bot_guard.is_some(),
|
||||
config: config_guard.clone(),
|
||||
@@ -140,7 +140,7 @@ impl TelegramService {
|
||||
/// Send message to user
|
||||
pub async fn send_message(&self, chat_id: i64, text: String) -> Result<()> {
|
||||
let bot_guard = self.bot.read().await;
|
||||
|
||||
|
||||
if let Some(bot) = bot_guard.as_ref() {
|
||||
bot.send_message(ChatId(chat_id), text).await?;
|
||||
Ok(())
|
||||
@@ -148,11 +148,16 @@ impl TelegramService {
|
||||
Err(anyhow::anyhow!("Bot is not running"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Send message to user with inline keyboard
|
||||
pub async fn send_message_with_keyboard(&self, chat_id: i64, text: String, keyboard: teloxide::types::InlineKeyboardMarkup) -> Result<()> {
|
||||
pub async fn send_message_with_keyboard(
|
||||
&self,
|
||||
chat_id: i64,
|
||||
text: String,
|
||||
keyboard: teloxide::types::InlineKeyboardMarkup,
|
||||
) -> Result<()> {
|
||||
let bot_guard = self.bot.read().await;
|
||||
|
||||
|
||||
if let Some(bot) = bot_guard.as_ref() {
|
||||
bot.send_message(ChatId(chat_id), text)
|
||||
.parse_mode(teloxide::types::ParseMode::Html)
|
||||
@@ -167,11 +172,11 @@ impl TelegramService {
|
||||
/// Send message to all admins
|
||||
pub async fn broadcast_to_admins(&self, text: String) -> Result<()> {
|
||||
let bot_guard = self.bot.read().await;
|
||||
|
||||
|
||||
if let Some(bot) = bot_guard.as_ref() {
|
||||
let user_repo = crate::database::repository::UserRepository::new(self.db.connection());
|
||||
let admins = user_repo.get_telegram_admins().await?;
|
||||
|
||||
|
||||
for admin in admins {
|
||||
if let Some(telegram_id) = admin.telegram_id {
|
||||
if let Err(e) = bot.send_message(ChatId(telegram_id), text.clone()).await {
|
||||
@@ -179,7 +184,7 @@ impl TelegramService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Bot is not running"))
|
||||
@@ -192,4 +197,4 @@ impl TelegramService {
|
||||
pub struct BotStatus {
|
||||
pub is_running: bool,
|
||||
pub config: Option<TelegramConfig>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
|
||||
|
||||
pub mod shadowsocks;
|
||||
pub mod trojan;
|
||||
pub mod vless;
|
||||
pub mod vmess;
|
||||
pub mod trojan;
|
||||
pub mod shadowsocks;
|
||||
|
||||
pub use shadowsocks::ShadowsocksUriBuilder;
|
||||
pub use trojan::TrojanUriBuilder;
|
||||
pub use vless::VlessUriBuilder;
|
||||
pub use vmess::VmessUriBuilder;
|
||||
pub use trojan::TrojanUriBuilder;
|
||||
pub use shadowsocks::ShadowsocksUriBuilder;
|
||||
|
||||
/// Common trait for all URI builders
|
||||
pub trait UriBuilder {
|
||||
/// Build URI string from client configuration data
|
||||
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError>;
|
||||
|
||||
|
||||
/// Validate configuration for this protocol
|
||||
fn validate_config(&self, config: &ClientConfigData) -> Result<(), UriGeneratorError> {
|
||||
if config.hostname.is_empty() {
|
||||
return Err(UriGeneratorError::MissingRequiredField("hostname".to_string()));
|
||||
return Err(UriGeneratorError::MissingRequiredField(
|
||||
"hostname".to_string(),
|
||||
));
|
||||
}
|
||||
if config.port <= 0 || config.port > 65535 {
|
||||
return Err(UriGeneratorError::InvalidConfiguration("Invalid port number".to_string()));
|
||||
return Err(UriGeneratorError::InvalidConfiguration(
|
||||
"Invalid port number".to_string(),
|
||||
));
|
||||
}
|
||||
if config.xray_user_id.is_empty() {
|
||||
return Err(UriGeneratorError::MissingRequiredField("xray_user_id".to_string()));
|
||||
return Err(UriGeneratorError::MissingRequiredField(
|
||||
"xray_user_id".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -32,28 +38,28 @@ pub trait UriBuilder {
|
||||
|
||||
/// Helper functions for URI building
|
||||
pub mod utils {
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
use crate::services::uri_generator::error::UriGeneratorError;
|
||||
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// URL encode a string safely
|
||||
pub fn url_encode(input: &str) -> String {
|
||||
urlencoding::encode(input).to_string()
|
||||
}
|
||||
|
||||
|
||||
/// Build query string from parameters
|
||||
pub fn build_query_string(params: &HashMap<String, String>) -> String {
|
||||
let mut query_parts: Vec<String> = Vec::new();
|
||||
|
||||
|
||||
for (key, value) in params {
|
||||
if !value.is_empty() {
|
||||
query_parts.push(format!("{}={}", url_encode(key), url_encode(value)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
query_parts.join("&")
|
||||
}
|
||||
|
||||
|
||||
/// Extract transport type from stream settings
|
||||
pub fn extract_transport_type(stream_settings: &Value) -> String {
|
||||
stream_settings
|
||||
@@ -62,7 +68,7 @@ pub mod utils {
|
||||
.unwrap_or("tcp")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
|
||||
/// Extract security type from stream settings
|
||||
pub fn extract_security_type(stream_settings: &Value, has_certificate: bool) -> String {
|
||||
if has_certificate {
|
||||
@@ -75,7 +81,7 @@ pub mod utils {
|
||||
"none".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Extract WebSocket path from stream settings
|
||||
pub fn extract_ws_path(stream_settings: &Value) -> Option<String> {
|
||||
stream_settings
|
||||
@@ -84,7 +90,7 @@ pub mod utils {
|
||||
.and_then(|p| p.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Extract WebSocket host from stream settings
|
||||
pub fn extract_ws_host(stream_settings: &Value) -> Option<String> {
|
||||
stream_settings
|
||||
@@ -94,7 +100,7 @@ pub mod utils {
|
||||
.and_then(|host| host.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Extract gRPC service name from stream settings
|
||||
pub fn extract_grpc_service_name(stream_settings: &Value) -> Option<String> {
|
||||
stream_settings
|
||||
@@ -103,23 +109,27 @@ pub mod utils {
|
||||
.and_then(|name| name.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Extract TLS SNI from stream settings
|
||||
pub fn extract_tls_sni(stream_settings: &Value, certificate_domain: Option<&str>) -> Option<String> {
|
||||
pub fn extract_tls_sni(
|
||||
stream_settings: &Value,
|
||||
certificate_domain: Option<&str>,
|
||||
) -> Option<String> {
|
||||
// Try stream settings first
|
||||
if let Some(sni) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("serverName"))
|
||||
.and_then(|sni| sni.as_str()) {
|
||||
.and_then(|sni| sni.as_str())
|
||||
{
|
||||
return Some(sni.to_string());
|
||||
}
|
||||
|
||||
|
||||
// Fall back to certificate domain
|
||||
certificate_domain.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Determine alias for the URI
|
||||
pub fn generate_alias(server_name: &str, template_name: &str) -> String {
|
||||
format!("{} - {}", server_name, template_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
use super::{UriBuilder, utils};
|
||||
use super::{utils, UriBuilder};
|
||||
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
|
||||
|
||||
pub struct ShadowsocksUriBuilder;
|
||||
|
||||
@@ -10,54 +10,56 @@ impl ShadowsocksUriBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
|
||||
/// Map Xray cipher type to Shadowsocks method name
|
||||
fn map_xray_cipher_to_shadowsocks_method(&self, cipher: &str) -> &str {
|
||||
match cipher {
|
||||
// AES GCM variants
|
||||
"AES_256_GCM" | "aes-256-gcm" => "aes-256-gcm",
|
||||
"AES_128_GCM" | "aes-128-gcm" => "aes-128-gcm",
|
||||
|
||||
// ChaCha20 variants
|
||||
"CHACHA20_POLY1305" | "chacha20-ietf-poly1305" | "chacha20-poly1305" => "chacha20-ietf-poly1305",
|
||||
|
||||
|
||||
// ChaCha20 variants
|
||||
"CHACHA20_POLY1305" | "chacha20-ietf-poly1305" | "chacha20-poly1305" => {
|
||||
"chacha20-ietf-poly1305"
|
||||
}
|
||||
|
||||
// AES CFB variants
|
||||
"AES_256_CFB" | "aes-256-cfb" => "aes-256-cfb",
|
||||
"AES_128_CFB" | "aes-128-cfb" => "aes-128-cfb",
|
||||
|
||||
|
||||
// Legacy ciphers
|
||||
"RC4_MD5" | "rc4-md5" => "rc4-md5",
|
||||
"AES_256_CTR" | "aes-256-ctr" => "aes-256-ctr",
|
||||
"AES_128_CTR" | "aes-128-ctr" => "aes-128-ctr",
|
||||
|
||||
|
||||
// Default to most secure and widely supported
|
||||
_ => "aes-256-gcm",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl UriBuilder for ShadowsocksUriBuilder {
|
||||
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
|
||||
self.validate_config(config)?;
|
||||
|
||||
|
||||
// Get cipher type from base_settings and map to Shadowsocks method
|
||||
let cipher = config.base_settings
|
||||
let cipher = config
|
||||
.base_settings
|
||||
.get("cipherType")
|
||||
.and_then(|c| c.as_str())
|
||||
.or_else(|| config.base_settings.get("method").and_then(|m| m.as_str()))
|
||||
.unwrap_or("AES_256_GCM");
|
||||
|
||||
|
||||
let method = self.map_xray_cipher_to_shadowsocks_method(cipher);
|
||||
|
||||
|
||||
// Shadowsocks SIP002 format: ss://base64(method:password)@hostname:port#remark
|
||||
// Use xray_user_id as password (following Marzban approach)
|
||||
let credentials = format!("{}:{}", method, config.xray_user_id);
|
||||
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
|
||||
|
||||
|
||||
// Generate alias for the URI
|
||||
let alias = utils::generate_alias(&config.server_name, &config.template_name);
|
||||
|
||||
|
||||
// Build simple SIP002 URI (no plugin parameters for standard Shadowsocks)
|
||||
let uri = format!(
|
||||
"ss://{}@{}:{}#{}",
|
||||
@@ -66,24 +68,30 @@ impl UriBuilder for ShadowsocksUriBuilder {
|
||||
config.port,
|
||||
utils::url_encode(&alias)
|
||||
);
|
||||
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
|
||||
|
||||
fn validate_config(&self, config: &ClientConfigData) -> Result<(), UriGeneratorError> {
|
||||
// Basic validation
|
||||
if config.hostname.is_empty() {
|
||||
return Err(UriGeneratorError::MissingRequiredField("hostname".to_string()));
|
||||
return Err(UriGeneratorError::MissingRequiredField(
|
||||
"hostname".to_string(),
|
||||
));
|
||||
}
|
||||
if config.port <= 0 || config.port > 65535 {
|
||||
return Err(UriGeneratorError::InvalidConfiguration("Invalid port number".to_string()));
|
||||
return Err(UriGeneratorError::InvalidConfiguration(
|
||||
"Invalid port number".to_string(),
|
||||
));
|
||||
}
|
||||
if config.xray_user_id.is_empty() {
|
||||
return Err(UriGeneratorError::MissingRequiredField("xray_user_id".to_string()));
|
||||
return Err(UriGeneratorError::MissingRequiredField(
|
||||
"xray_user_id".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Shadowsocks uses xray_user_id as password, already validated above
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -93,4 +101,3 @@ impl Default for ShadowsocksUriBuilder {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
use super::{UriBuilder, utils};
|
||||
use super::{utils, UriBuilder};
|
||||
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
|
||||
|
||||
pub struct TrojanUriBuilder;
|
||||
|
||||
@@ -15,32 +15,32 @@ impl TrojanUriBuilder {
|
||||
impl UriBuilder for TrojanUriBuilder {
|
||||
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
|
||||
self.validate_config(config)?;
|
||||
|
||||
|
||||
// Trojan uses xray_user_id as password
|
||||
let password = &config.xray_user_id;
|
||||
|
||||
|
||||
// Apply variable substitution to stream settings
|
||||
let stream_settings = if !config.variable_values.is_null() {
|
||||
apply_variables(&config.stream_settings, &config.variable_values)?
|
||||
} else {
|
||||
config.stream_settings.clone()
|
||||
};
|
||||
|
||||
|
||||
let mut params = HashMap::new();
|
||||
|
||||
|
||||
// Determine security layer (Trojan typically uses TLS)
|
||||
let has_certificate = config.certificate_domain.is_some();
|
||||
let security = utils::extract_security_type(&stream_settings, has_certificate);
|
||||
|
||||
|
||||
// Trojan usually requires TLS, but allow other security types
|
||||
if security != "none" {
|
||||
params.insert("security".to_string(), security.clone());
|
||||
}
|
||||
|
||||
|
||||
// Transport type - always specify explicitly
|
||||
let transport_type = utils::extract_transport_type(&stream_settings);
|
||||
params.insert("type".to_string(), transport_type.clone());
|
||||
|
||||
|
||||
// Transport-specific parameters
|
||||
match transport_type.as_str() {
|
||||
"ws" => {
|
||||
@@ -50,48 +50,53 @@ impl UriBuilder for TrojanUriBuilder {
|
||||
if let Some(host) = utils::extract_ws_host(&stream_settings) {
|
||||
params.insert("host".to_string(), host);
|
||||
}
|
||||
},
|
||||
}
|
||||
"grpc" => {
|
||||
if let Some(service_name) = utils::extract_grpc_service_name(&stream_settings) {
|
||||
params.insert("serviceName".to_string(), service_name);
|
||||
}
|
||||
// gRPC mode for Trojan
|
||||
params.insert("mode".to_string(), "gun".to_string());
|
||||
},
|
||||
}
|
||||
"tcp" => {
|
||||
// Check for HTTP header type
|
||||
if let Some(header_type) = stream_settings
|
||||
.get("tcpSettings")
|
||||
.and_then(|tcp| tcp.get("header"))
|
||||
.and_then(|header| header.get("type"))
|
||||
.and_then(|t| t.as_str()) {
|
||||
.and_then(|t| t.as_str())
|
||||
{
|
||||
if header_type != "none" {
|
||||
params.insert("headerType".to_string(), header_type.to_string());
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {} // Other transport types
|
||||
}
|
||||
|
||||
|
||||
// TLS/Security specific parameters
|
||||
if security == "tls" || security == "reality" {
|
||||
if let Some(sni) = utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref()) {
|
||||
if let Some(sni) =
|
||||
utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref())
|
||||
{
|
||||
params.insert("sni".to_string(), sni);
|
||||
}
|
||||
|
||||
|
||||
// TLS fingerprint
|
||||
if let Some(fp) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("fingerprint"))
|
||||
.and_then(|fp| fp.as_str()) {
|
||||
.and_then(|fp| fp.as_str())
|
||||
{
|
||||
params.insert("fp".to_string(), fp.to_string());
|
||||
}
|
||||
|
||||
|
||||
// ALPN
|
||||
if let Some(alpn) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("alpn"))
|
||||
.and_then(|alpn| alpn.as_array()) {
|
||||
.and_then(|alpn| alpn.as_array())
|
||||
{
|
||||
let alpn_str = alpn
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
@@ -101,46 +106,47 @@ impl UriBuilder for TrojanUriBuilder {
|
||||
params.insert("alpn".to_string(), alpn_str);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Allow insecure connections (optional)
|
||||
if let Some(allow_insecure) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("allowInsecure"))
|
||||
.and_then(|ai| ai.as_bool()) {
|
||||
.and_then(|ai| ai.as_bool())
|
||||
{
|
||||
if allow_insecure {
|
||||
params.insert("allowInsecure".to_string(), "1".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// REALITY specific parameters
|
||||
if security == "reality" {
|
||||
if let Some(pbk) = stream_settings
|
||||
.get("realitySettings")
|
||||
.and_then(|reality| reality.get("publicKey"))
|
||||
.and_then(|pbk| pbk.as_str()) {
|
||||
.and_then(|pbk| pbk.as_str())
|
||||
{
|
||||
params.insert("pbk".to_string(), pbk.to_string());
|
||||
}
|
||||
|
||||
|
||||
if let Some(sid) = stream_settings
|
||||
.get("realitySettings")
|
||||
.and_then(|reality| reality.get("shortId"))
|
||||
.and_then(|sid| sid.as_str()) {
|
||||
.and_then(|sid| sid.as_str())
|
||||
{
|
||||
params.insert("sid".to_string(), sid.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Flow control for XTLS (if supported)
|
||||
if let Some(flow) = stream_settings
|
||||
.get("flow")
|
||||
.and_then(|f| f.as_str()) {
|
||||
if let Some(flow) = stream_settings.get("flow").and_then(|f| f.as_str()) {
|
||||
params.insert("flow".to_string(), flow.to_string());
|
||||
}
|
||||
|
||||
|
||||
// Build the URI
|
||||
let query_string = utils::build_query_string(¶ms);
|
||||
let alias = utils::generate_alias(&config.server_name, &config.template_name);
|
||||
|
||||
|
||||
let uri = if query_string.is_empty() {
|
||||
format!(
|
||||
"trojan://{}@{}:{}#{}",
|
||||
@@ -159,24 +165,30 @@ impl UriBuilder for TrojanUriBuilder {
|
||||
utils::url_encode(&alias)
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
|
||||
|
||||
fn validate_config(&self, config: &ClientConfigData) -> Result<(), UriGeneratorError> {
|
||||
// Basic validation
|
||||
if config.hostname.is_empty() {
|
||||
return Err(UriGeneratorError::MissingRequiredField("hostname".to_string()));
|
||||
return Err(UriGeneratorError::MissingRequiredField(
|
||||
"hostname".to_string(),
|
||||
));
|
||||
}
|
||||
if config.port <= 0 || config.port > 65535 {
|
||||
return Err(UriGeneratorError::InvalidConfiguration("Invalid port number".to_string()));
|
||||
return Err(UriGeneratorError::InvalidConfiguration(
|
||||
"Invalid port number".to_string(),
|
||||
));
|
||||
}
|
||||
if config.xray_user_id.is_empty() {
|
||||
return Err(UriGeneratorError::MissingRequiredField("xray_user_id".to_string()));
|
||||
return Err(UriGeneratorError::MissingRequiredField(
|
||||
"xray_user_id".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Trojan uses xray_user_id as password, already validated above
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -191,7 +203,7 @@ impl Default for TrojanUriBuilder {
|
||||
fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGeneratorError> {
|
||||
let template_str = template.to_string();
|
||||
let mut result = template_str;
|
||||
|
||||
|
||||
if let Value::Object(var_map) = variables {
|
||||
for (key, value) in var_map {
|
||||
let placeholder = format!("${{{}}}", key);
|
||||
@@ -204,7 +216,7 @@ fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGene
|
||||
result = result.replace(&placeholder, &replacement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
serde_json::from_str(&result)
|
||||
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
use super::{UriBuilder, utils};
|
||||
use super::{utils, UriBuilder};
|
||||
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
|
||||
|
||||
pub struct VlessUriBuilder;
|
||||
|
||||
@@ -15,7 +15,7 @@ impl VlessUriBuilder {
|
||||
impl UriBuilder for VlessUriBuilder {
|
||||
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
|
||||
self.validate_config(config)?;
|
||||
|
||||
|
||||
// Apply variable substitution to stream settings
|
||||
let stream_settings = if !config.variable_values.is_null() {
|
||||
// Simple variable substitution for stream settings
|
||||
@@ -23,23 +23,23 @@ impl UriBuilder for VlessUriBuilder {
|
||||
} else {
|
||||
config.stream_settings.clone()
|
||||
};
|
||||
|
||||
|
||||
let mut params = HashMap::new();
|
||||
|
||||
|
||||
// VLESS always uses no encryption
|
||||
params.insert("encryption".to_string(), "none".to_string());
|
||||
|
||||
|
||||
// Determine security layer
|
||||
let has_certificate = config.certificate_domain.is_some();
|
||||
let security = utils::extract_security_type(&stream_settings, has_certificate);
|
||||
if security != "none" {
|
||||
params.insert("security".to_string(), security.clone());
|
||||
}
|
||||
|
||||
|
||||
// Transport type - always specify explicitly
|
||||
let transport_type = utils::extract_transport_type(&stream_settings);
|
||||
params.insert("type".to_string(), transport_type.clone());
|
||||
|
||||
|
||||
// Transport-specific parameters
|
||||
match transport_type.as_str() {
|
||||
"ws" => {
|
||||
@@ -49,72 +49,76 @@ impl UriBuilder for VlessUriBuilder {
|
||||
if let Some(host) = utils::extract_ws_host(&stream_settings) {
|
||||
params.insert("host".to_string(), host);
|
||||
}
|
||||
},
|
||||
}
|
||||
"grpc" => {
|
||||
if let Some(service_name) = utils::extract_grpc_service_name(&stream_settings) {
|
||||
params.insert("serviceName".to_string(), service_name);
|
||||
}
|
||||
// Default gRPC mode
|
||||
params.insert("mode".to_string(), "gun".to_string());
|
||||
},
|
||||
}
|
||||
"tcp" => {
|
||||
// Check for HTTP header type
|
||||
if let Some(header_type) = stream_settings
|
||||
.get("tcpSettings")
|
||||
.and_then(|tcp| tcp.get("header"))
|
||||
.and_then(|header| header.get("type"))
|
||||
.and_then(|t| t.as_str()) {
|
||||
.and_then(|t| t.as_str())
|
||||
{
|
||||
if header_type != "none" {
|
||||
params.insert("headerType".to_string(), header_type.to_string());
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {} // Other transport types can be added as needed
|
||||
}
|
||||
|
||||
|
||||
// TLS/Security specific parameters
|
||||
if security == "tls" || security == "reality" {
|
||||
if let Some(sni) = utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref()) {
|
||||
if let Some(sni) =
|
||||
utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref())
|
||||
{
|
||||
params.insert("sni".to_string(), sni);
|
||||
}
|
||||
|
||||
|
||||
// TLS fingerprint
|
||||
if let Some(fp) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("fingerprint"))
|
||||
.and_then(|fp| fp.as_str()) {
|
||||
.and_then(|fp| fp.as_str())
|
||||
{
|
||||
params.insert("fp".to_string(), fp.to_string());
|
||||
}
|
||||
|
||||
|
||||
// REALITY specific parameters
|
||||
if security == "reality" {
|
||||
if let Some(pbk) = stream_settings
|
||||
.get("realitySettings")
|
||||
.and_then(|reality| reality.get("publicKey"))
|
||||
.and_then(|pbk| pbk.as_str()) {
|
||||
.and_then(|pbk| pbk.as_str())
|
||||
{
|
||||
params.insert("pbk".to_string(), pbk.to_string());
|
||||
}
|
||||
|
||||
|
||||
if let Some(sid) = stream_settings
|
||||
.get("realitySettings")
|
||||
.and_then(|reality| reality.get("shortId"))
|
||||
.and_then(|sid| sid.as_str()) {
|
||||
.and_then(|sid| sid.as_str())
|
||||
{
|
||||
params.insert("sid".to_string(), sid.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Flow control for XTLS
|
||||
if let Some(flow) = stream_settings
|
||||
.get("flow")
|
||||
.and_then(|f| f.as_str()) {
|
||||
if let Some(flow) = stream_settings.get("flow").and_then(|f| f.as_str()) {
|
||||
params.insert("flow".to_string(), flow.to_string());
|
||||
}
|
||||
|
||||
|
||||
// Build the URI
|
||||
let query_string = utils::build_query_string(¶ms);
|
||||
let alias = utils::generate_alias(&config.server_name, &config.template_name);
|
||||
|
||||
|
||||
let uri = if query_string.is_empty() {
|
||||
format!(
|
||||
"vless://{}@{}:{}#{}",
|
||||
@@ -133,7 +137,7 @@ impl UriBuilder for VlessUriBuilder {
|
||||
utils::url_encode(&alias)
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
}
|
||||
@@ -148,7 +152,7 @@ impl Default for VlessUriBuilder {
|
||||
fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGeneratorError> {
|
||||
let template_str = template.to_string();
|
||||
let mut result = template_str;
|
||||
|
||||
|
||||
if let Value::Object(var_map) = variables {
|
||||
for (key, value) in var_map {
|
||||
let placeholder = format!("${{{}}}", key);
|
||||
@@ -161,7 +165,7 @@ fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGene
|
||||
result = result.replace(&placeholder, &replacement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
serde_json::from_str(&result)
|
||||
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use serde_json::{Value, json};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
use super::{UriBuilder, utils};
|
||||
use super::{utils, UriBuilder};
|
||||
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
|
||||
|
||||
pub struct VmessUriBuilder;
|
||||
|
||||
@@ -11,20 +11,23 @@ impl VmessUriBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
|
||||
/// Build VMess URI in Base64 JSON format (following Marzban approach)
|
||||
fn build_base64_json_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
|
||||
fn build_base64_json_uri(
|
||||
&self,
|
||||
config: &ClientConfigData,
|
||||
) -> Result<String, UriGeneratorError> {
|
||||
// Apply variable substitution to stream settings
|
||||
let stream_settings = if !config.variable_values.is_null() {
|
||||
apply_variables(&config.stream_settings, &config.variable_values)?
|
||||
} else {
|
||||
config.stream_settings.clone()
|
||||
};
|
||||
|
||||
|
||||
let transport_type = utils::extract_transport_type(&stream_settings);
|
||||
let has_certificate = config.certificate_domain.is_some();
|
||||
let security = utils::extract_security_type(&stream_settings, has_certificate);
|
||||
|
||||
|
||||
// Build VMess JSON configuration following Marzban structure
|
||||
let mut vmess_config = json!({
|
||||
"add": config.hostname,
|
||||
@@ -40,7 +43,7 @@ impl VmessUriBuilder {
|
||||
"type": "none",
|
||||
"v": "2"
|
||||
});
|
||||
|
||||
|
||||
// Transport-specific settings
|
||||
match transport_type.as_str() {
|
||||
"ws" => {
|
||||
@@ -50,23 +53,24 @@ impl VmessUriBuilder {
|
||||
if let Some(host) = utils::extract_ws_host(&stream_settings) {
|
||||
vmess_config["host"] = Value::String(host);
|
||||
}
|
||||
},
|
||||
}
|
||||
"grpc" => {
|
||||
if let Some(service_name) = utils::extract_grpc_service_name(&stream_settings) {
|
||||
vmess_config["path"] = Value::String(service_name);
|
||||
}
|
||||
// For gRPC in VMess, use "gun" type
|
||||
vmess_config["type"] = Value::String("gun".to_string());
|
||||
},
|
||||
}
|
||||
"tcp" => {
|
||||
// Check for HTTP header type
|
||||
if let Some(header_type) = stream_settings
|
||||
.get("tcpSettings")
|
||||
.and_then(|tcp| tcp.get("header"))
|
||||
.and_then(|header| header.get("type"))
|
||||
.and_then(|t| t.as_str()) {
|
||||
.and_then(|t| t.as_str())
|
||||
{
|
||||
vmess_config["type"] = Value::String(header_type.to_string());
|
||||
|
||||
|
||||
// If HTTP headers, get host and path
|
||||
if header_type == "http" {
|
||||
if let Some(host) = stream_settings
|
||||
@@ -77,10 +81,11 @@ impl VmessUriBuilder {
|
||||
.and_then(|headers| headers.get("Host"))
|
||||
.and_then(|host| host.as_array())
|
||||
.and_then(|arr| arr.first())
|
||||
.and_then(|h| h.as_str()) {
|
||||
.and_then(|h| h.as_str())
|
||||
{
|
||||
vmess_config["host"] = Value::String(host.to_string());
|
||||
}
|
||||
|
||||
|
||||
if let Some(path) = stream_settings
|
||||
.get("tcpSettings")
|
||||
.and_then(|tcp| tcp.get("header"))
|
||||
@@ -88,34 +93,39 @@ impl VmessUriBuilder {
|
||||
.and_then(|request| request.get("path"))
|
||||
.and_then(|path| path.as_array())
|
||||
.and_then(|arr| arr.first())
|
||||
.and_then(|p| p.as_str()) {
|
||||
.and_then(|p| p.as_str())
|
||||
{
|
||||
vmess_config["path"] = Value::String(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {} // Other transport types
|
||||
}
|
||||
|
||||
|
||||
// TLS settings
|
||||
if security != "none" {
|
||||
if let Some(sni) = utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref()) {
|
||||
if let Some(sni) =
|
||||
utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref())
|
||||
{
|
||||
vmess_config["sni"] = Value::String(sni);
|
||||
}
|
||||
|
||||
|
||||
// TLS fingerprint
|
||||
if let Some(fp) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("fingerprint"))
|
||||
.and_then(|fp| fp.as_str()) {
|
||||
.and_then(|fp| fp.as_str())
|
||||
{
|
||||
vmess_config["fp"] = Value::String(fp.to_string());
|
||||
}
|
||||
|
||||
|
||||
// ALPN
|
||||
if let Some(alpn) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("alpn"))
|
||||
.and_then(|alpn| alpn.as_array()) {
|
||||
.and_then(|alpn| alpn.as_array())
|
||||
{
|
||||
let alpn_str = alpn
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
@@ -126,41 +136,44 @@ impl VmessUriBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Convert to JSON string and encode in Base64
|
||||
let json_string = vmess_config.to_string();
|
||||
let encoded = general_purpose::STANDARD.encode(json_string.as_bytes());
|
||||
|
||||
|
||||
Ok(format!("vmess://{}", encoded))
|
||||
}
|
||||
|
||||
|
||||
/// Build VMess URI in query parameter format (alternative)
|
||||
fn build_query_param_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
|
||||
fn build_query_param_uri(
|
||||
&self,
|
||||
config: &ClientConfigData,
|
||||
) -> Result<String, UriGeneratorError> {
|
||||
// Apply variable substitution to stream settings
|
||||
let stream_settings = if !config.variable_values.is_null() {
|
||||
apply_variables(&config.stream_settings, &config.variable_values)?
|
||||
} else {
|
||||
config.stream_settings.clone()
|
||||
};
|
||||
|
||||
|
||||
let mut params = HashMap::new();
|
||||
|
||||
|
||||
// VMess uses auto encryption
|
||||
params.insert("encryption".to_string(), "auto".to_string());
|
||||
|
||||
|
||||
// Determine security layer
|
||||
let has_certificate = config.certificate_domain.is_some();
|
||||
let security = utils::extract_security_type(&stream_settings, has_certificate);
|
||||
if security != "none" {
|
||||
params.insert("security".to_string(), security.clone());
|
||||
}
|
||||
|
||||
|
||||
// Transport type
|
||||
let transport_type = utils::extract_transport_type(&stream_settings);
|
||||
if transport_type != "tcp" {
|
||||
params.insert("type".to_string(), transport_type.clone());
|
||||
}
|
||||
|
||||
|
||||
// Transport-specific parameters
|
||||
match transport_type.as_str() {
|
||||
"ws" => {
|
||||
@@ -170,34 +183,37 @@ impl VmessUriBuilder {
|
||||
if let Some(host) = utils::extract_ws_host(&stream_settings) {
|
||||
params.insert("host".to_string(), host);
|
||||
}
|
||||
},
|
||||
}
|
||||
"grpc" => {
|
||||
if let Some(service_name) = utils::extract_grpc_service_name(&stream_settings) {
|
||||
params.insert("serviceName".to_string(), service_name);
|
||||
}
|
||||
params.insert("mode".to_string(), "gun".to_string());
|
||||
},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
// TLS specific parameters
|
||||
if security != "none" {
|
||||
if let Some(sni) = utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref()) {
|
||||
if let Some(sni) =
|
||||
utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref())
|
||||
{
|
||||
params.insert("sni".to_string(), sni);
|
||||
}
|
||||
|
||||
|
||||
if let Some(fp) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("fingerprint"))
|
||||
.and_then(|fp| fp.as_str()) {
|
||||
.and_then(|fp| fp.as_str())
|
||||
{
|
||||
params.insert("fp".to_string(), fp.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Build the URI
|
||||
let query_string = utils::build_query_string(¶ms);
|
||||
let alias = utils::generate_alias(&config.server_name, &config.template_name);
|
||||
|
||||
|
||||
let uri = if query_string.is_empty() {
|
||||
format!(
|
||||
"vmess://{}@{}:{}#{}",
|
||||
@@ -216,7 +232,7 @@ impl VmessUriBuilder {
|
||||
utils::url_encode(&alias)
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
}
|
||||
@@ -224,7 +240,7 @@ impl VmessUriBuilder {
|
||||
impl UriBuilder for VmessUriBuilder {
|
||||
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
|
||||
self.validate_config(config)?;
|
||||
|
||||
|
||||
// Prefer Base64 JSON format as it's more widely supported
|
||||
self.build_base64_json_uri(config)
|
||||
}
|
||||
@@ -240,7 +256,7 @@ impl Default for VmessUriBuilder {
|
||||
fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGeneratorError> {
|
||||
let template_str = template.to_string();
|
||||
let mut result = template_str;
|
||||
|
||||
|
||||
if let Value::Object(var_map) = variables {
|
||||
for (key, value) in var_map {
|
||||
let placeholder = format!("${{{}}}", key);
|
||||
@@ -253,7 +269,7 @@ fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGene
|
||||
result = result.replace(&placeholder, &replacement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
serde_json::from_str(&result)
|
||||
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,4 @@ impl From<serde_json::Error> for UriGeneratorError {
|
||||
// fn from(err: urlencoding::EncodingError) -> Self {
|
||||
// UriGeneratorError::UriEncoding(err.to_string())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -6,7 +6,9 @@ use uuid::Uuid;
|
||||
pub mod builders;
|
||||
pub mod error;
|
||||
|
||||
use builders::{UriBuilder, VlessUriBuilder, VmessUriBuilder, TrojanUriBuilder, ShadowsocksUriBuilder};
|
||||
use builders::{
|
||||
ShadowsocksUriBuilder, TrojanUriBuilder, UriBuilder, VlessUriBuilder, VmessUriBuilder,
|
||||
};
|
||||
use error::UriGeneratorError;
|
||||
|
||||
/// Complete client configuration data aggregated from database
|
||||
@@ -17,23 +19,23 @@ pub struct ClientConfigData {
|
||||
pub xray_user_id: String,
|
||||
pub password: Option<String>,
|
||||
pub level: i32,
|
||||
|
||||
|
||||
// Server connection
|
||||
pub hostname: String,
|
||||
pub port: i32,
|
||||
|
||||
|
||||
// Protocol & transport
|
||||
pub protocol: String,
|
||||
pub stream_settings: Value,
|
||||
pub base_settings: Value,
|
||||
|
||||
|
||||
// Security
|
||||
pub certificate_domain: Option<String>,
|
||||
pub requires_tls: bool,
|
||||
|
||||
|
||||
// Variable substitution
|
||||
pub variable_values: Value,
|
||||
|
||||
|
||||
// Metadata
|
||||
pub server_name: String,
|
||||
pub inbound_tag: String,
|
||||
@@ -60,36 +62,40 @@ impl UriGeneratorService {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
|
||||
/// Generate URI for specific protocol and configuration
|
||||
pub fn generate_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
|
||||
let protocol = config.protocol.as_str();
|
||||
|
||||
|
||||
match protocol {
|
||||
"vless" => {
|
||||
let builder = VlessUriBuilder::new();
|
||||
builder.build_uri(config)
|
||||
},
|
||||
}
|
||||
"vmess" => {
|
||||
let builder = VmessUriBuilder::new();
|
||||
builder.build_uri(config)
|
||||
},
|
||||
}
|
||||
"trojan" => {
|
||||
let builder = TrojanUriBuilder::new();
|
||||
builder.build_uri(config)
|
||||
},
|
||||
}
|
||||
"shadowsocks" => {
|
||||
let builder = ShadowsocksUriBuilder::new();
|
||||
builder.build_uri(config)
|
||||
},
|
||||
}
|
||||
_ => Err(UriGeneratorError::UnsupportedProtocol(protocol.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Generate complete client configuration
|
||||
pub fn generate_client_config(&self, user_id: Uuid, config: &ClientConfigData) -> Result<ClientConfig, UriGeneratorError> {
|
||||
pub fn generate_client_config(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
config: &ClientConfigData,
|
||||
) -> Result<ClientConfig, UriGeneratorError> {
|
||||
let uri = self.generate_uri(config)?;
|
||||
|
||||
|
||||
Ok(ClientConfig {
|
||||
user_id,
|
||||
server_name: config.server_name.clone(),
|
||||
@@ -100,12 +106,16 @@ impl UriGeneratorService {
|
||||
qr_code: None, // TODO: Implement QR code generation if needed
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Apply variable substitution to JSON values
|
||||
pub fn apply_variable_substitution(&self, template: &Value, variables: &Value) -> Result<Value, UriGeneratorError> {
|
||||
pub fn apply_variable_substitution(
|
||||
&self,
|
||||
template: &Value,
|
||||
variables: &Value,
|
||||
) -> Result<Value, UriGeneratorError> {
|
||||
let template_str = template.to_string();
|
||||
let mut result = template_str;
|
||||
|
||||
|
||||
if let Value::Object(var_map) = variables {
|
||||
for (key, value) in var_map {
|
||||
let placeholder = format!("${{{}}}", key);
|
||||
@@ -118,7 +128,7 @@ impl UriGeneratorService {
|
||||
result = result.replace(&placeholder, &replacement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
serde_json::from_str(&result)
|
||||
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
|
||||
}
|
||||
@@ -128,4 +138,4 @@ impl Default for UriGeneratorService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde_json::Value;
|
||||
use xray_core::Client;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use xray_core::Client;
|
||||
|
||||
// Import submodules from the same directory
|
||||
use super::stats::StatsClient;
|
||||
use super::inbounds::InboundClient;
|
||||
use super::stats::StatsClient;
|
||||
use super::users::UserClient;
|
||||
|
||||
/// Xray gRPC client wrapper
|
||||
@@ -22,20 +22,17 @@ impl XrayClient {
|
||||
pub async fn connect(endpoint: &str) -> Result<Self> {
|
||||
// Apply a 5-second timeout to the connection attempt
|
||||
let connect_future = Client::from_url(endpoint);
|
||||
|
||||
|
||||
match timeout(Duration::from_secs(5), connect_future).await {
|
||||
Ok(Ok(client)) => {
|
||||
Ok(Self {
|
||||
endpoint: endpoint.to_string(),
|
||||
client: Arc::new(client),
|
||||
})
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
Err(anyhow!("Failed to connect to Xray at {}: {}", endpoint, e))
|
||||
},
|
||||
Err(_) => {
|
||||
Err(anyhow!("Connection to Xray at {} timed out after 5 seconds", endpoint))
|
||||
}
|
||||
Ok(Ok(client)) => Ok(Self {
|
||||
endpoint: endpoint.to_string(),
|
||||
client: Arc::new(client),
|
||||
}),
|
||||
Ok(Err(e)) => Err(anyhow!("Failed to connect to Xray at {}: {}", endpoint, e)),
|
||||
Err(_) => Err(anyhow!(
|
||||
"Connection to Xray at {} timed out after 5 seconds",
|
||||
endpoint
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +49,10 @@ impl XrayClient {
|
||||
}
|
||||
|
||||
/// Restart Xray with new configuration
|
||||
pub async fn restart_with_config(&self, config: &crate::services::xray::XrayConfig) -> Result<()> {
|
||||
pub async fn restart_with_config(
|
||||
&self,
|
||||
config: &crate::services::xray::XrayConfig,
|
||||
) -> Result<()> {
|
||||
let inbound_client = InboundClient::new(self.endpoint.clone(), &*self.client);
|
||||
inbound_client.restart_with_config(config).await
|
||||
}
|
||||
@@ -64,15 +64,30 @@ impl XrayClient {
|
||||
}
|
||||
|
||||
/// Add inbound configuration with TLS certificate
|
||||
pub async fn add_inbound_with_certificate(&self, inbound: &Value, cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
pub async fn add_inbound_with_certificate(
|
||||
&self,
|
||||
inbound: &Value,
|
||||
cert_pem: Option<&str>,
|
||||
key_pem: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let inbound_client = InboundClient::new(self.endpoint.clone(), &*self.client);
|
||||
inbound_client.add_inbound_with_certificate(inbound, None, cert_pem, key_pem).await
|
||||
inbound_client
|
||||
.add_inbound_with_certificate(inbound, None, cert_pem, key_pem)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Add inbound configuration with users and TLS certificate
|
||||
pub async fn add_inbound_with_users_and_certificate(&self, inbound: &Value, users: &[Value], cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
pub async fn add_inbound_with_users_and_certificate(
|
||||
&self,
|
||||
inbound: &Value,
|
||||
users: &[Value],
|
||||
cert_pem: Option<&str>,
|
||||
key_pem: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let inbound_client = InboundClient::new(self.endpoint.clone(), &*self.client);
|
||||
inbound_client.add_inbound_with_certificate(inbound, Some(users), cert_pem, key_pem).await
|
||||
inbound_client
|
||||
.add_inbound_with_certificate(inbound, Some(users), cert_pem, key_pem)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Remove inbound by tag
|
||||
@@ -97,4 +112,4 @@ impl XrayClient {
|
||||
pub fn endpoint(&self) -> &str {
|
||||
&self.endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,25 +171,26 @@ impl XrayConfig {
|
||||
dns: None,
|
||||
routing: Some(RoutingConfig {
|
||||
domain_strategy: Some("IPIfNonMatch".to_string()),
|
||||
rules: vec![
|
||||
RoutingRule {
|
||||
rule_type: "field".to_string(),
|
||||
domain: None,
|
||||
ip: Some(vec!["geoip:private".to_string()]),
|
||||
port: None,
|
||||
outbound_tag: "direct".to_string(),
|
||||
}
|
||||
],
|
||||
rules: vec![RoutingRule {
|
||||
rule_type: "field".to_string(),
|
||||
domain: None,
|
||||
ip: Some(vec!["geoip:private".to_string()]),
|
||||
port: None,
|
||||
outbound_tag: "direct".to_string(),
|
||||
}],
|
||||
}),
|
||||
policy: Some(PolicyConfig {
|
||||
levels: {
|
||||
let mut levels = HashMap::new();
|
||||
levels.insert("0".to_string(), PolicyLevel {
|
||||
handshake_timeout: Some(4),
|
||||
conn_idle: Some(300),
|
||||
uplink_only: Some(2),
|
||||
downlink_only: Some(5),
|
||||
});
|
||||
levels.insert(
|
||||
"0".to_string(),
|
||||
PolicyLevel {
|
||||
handshake_timeout: Some(4),
|
||||
conn_idle: Some(300),
|
||||
uplink_only: Some(2),
|
||||
downlink_only: Some(5),
|
||||
},
|
||||
);
|
||||
levels
|
||||
},
|
||||
system: Some(SystemPolicy {
|
||||
@@ -282,4 +283,4 @@ impl Default for XrayConfig {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,44 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{anyhow, Result};
|
||||
use prost::Message;
|
||||
use serde_json::Value;
|
||||
use uuid;
|
||||
use xray_core::{
|
||||
tonic::Request,
|
||||
app::proxyman::command::{AddInboundRequest, RemoveInboundRequest},
|
||||
core::InboundHandlerConfig,
|
||||
common::serial::TypedMessage,
|
||||
common::protocol::User,
|
||||
app::proxyman::ReceiverConfig,
|
||||
common::net::{PortList, PortRange, IpOrDomain, ip_or_domain::Address, Network},
|
||||
transport::internet::StreamConfig,
|
||||
transport::internet::tls::{Config as TlsConfig, Certificate as TlsCertificate},
|
||||
common::net::{ip_or_domain::Address, IpOrDomain, Network, PortList, PortRange},
|
||||
common::protocol::User,
|
||||
common::serial::TypedMessage,
|
||||
core::InboundHandlerConfig,
|
||||
prost_types,
|
||||
proxy::shadowsocks::ServerConfig as ShadowsocksServerConfig,
|
||||
proxy::shadowsocks::{Account as ShadowsocksAccount, CipherType},
|
||||
proxy::trojan::Account as TrojanAccount,
|
||||
proxy::trojan::ServerConfig as TrojanServerConfig,
|
||||
proxy::vless::inbound::Config as VlessInboundConfig,
|
||||
proxy::vless::Account as VlessAccount,
|
||||
proxy::vmess::inbound::Config as VmessInboundConfig,
|
||||
proxy::vmess::Account as VmessAccount,
|
||||
proxy::trojan::ServerConfig as TrojanServerConfig,
|
||||
proxy::trojan::Account as TrojanAccount,
|
||||
proxy::shadowsocks::ServerConfig as ShadowsocksServerConfig,
|
||||
proxy::shadowsocks::{Account as ShadowsocksAccount, CipherType},
|
||||
tonic::Request,
|
||||
transport::internet::tls::{Certificate as TlsCertificate, Config as TlsConfig},
|
||||
transport::internet::StreamConfig,
|
||||
Client,
|
||||
prost_types,
|
||||
};
|
||||
use prost::Message;
|
||||
|
||||
/// Convert PEM format to DER (x509) format
|
||||
fn pem_to_der(pem_data: &str) -> Result<Vec<u8>> {
|
||||
// Remove PEM headers and whitespace, then decode base64
|
||||
let base64_data: String = pem_data.lines()
|
||||
let base64_data: String = pem_data
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with("-----") && !line.trim().is_empty())
|
||||
.map(|line| line.trim())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("");
|
||||
|
||||
|
||||
tracing::debug!("PEM to DER conversion: {} bytes", base64_data.len());
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
general_purpose::STANDARD.decode(&base64_data)
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
general_purpose::STANDARD
|
||||
.decode(&base64_data)
|
||||
.map_err(|e| anyhow!("Failed to decode base64 PEM data: {}", e))
|
||||
}
|
||||
|
||||
@@ -52,22 +54,32 @@ impl<'a> InboundClient<'a> {
|
||||
|
||||
/// Add inbound configuration
|
||||
pub async fn add_inbound(&self, inbound: &Value) -> Result<()> {
|
||||
self.add_inbound_with_certificate(inbound, None, None, None).await
|
||||
self.add_inbound_with_certificate(inbound, None, None, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Add inbound configuration with TLS certificate and users
|
||||
pub async fn add_inbound_with_certificate(&self, inbound: &Value, users: Option<&[Value]>, cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
pub async fn add_inbound_with_certificate(
|
||||
&self,
|
||||
inbound: &Value,
|
||||
users: Option<&[Value]>,
|
||||
cert_pem: Option<&str>,
|
||||
key_pem: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let tag = inbound["tag"].as_str().unwrap_or("").to_string();
|
||||
let port = inbound["port"].as_u64().unwrap_or(8080) as u32;
|
||||
let protocol = inbound["protocol"].as_str().unwrap_or("vless");
|
||||
let _user_count = users.map_or(0, |u| u.len());
|
||||
|
||||
|
||||
tracing::info!(
|
||||
"Adding inbound '{}' with protocol={}, port={}, has_cert={}, has_key={}",
|
||||
tag, protocol, port, cert_pem.is_some(), key_pem.is_some()
|
||||
"Adding inbound '{}' with protocol={}, port={}, has_cert={}, has_key={}",
|
||||
tag,
|
||||
protocol,
|
||||
port,
|
||||
cert_pem.is_some(),
|
||||
key_pem.is_some()
|
||||
);
|
||||
|
||||
|
||||
|
||||
// Create receiver configuration (port binding) - use simple port number
|
||||
let port_list = PortList {
|
||||
range: vec![PortRange {
|
||||
@@ -80,39 +92,42 @@ impl<'a> InboundClient<'a> {
|
||||
let stream_settings = if cert_pem.is_some() && key_pem.is_some() {
|
||||
let cert_pem = cert_pem.unwrap();
|
||||
let key_pem = key_pem.unwrap();
|
||||
|
||||
|
||||
// Create TLS certificate exactly like working example - PEM content as bytes
|
||||
let tls_cert = TlsCertificate {
|
||||
certificate: cert_pem.as_bytes().to_vec(), // PEM content as bytes like working example
|
||||
key: key_pem.as_bytes().to_vec(), // PEM content as bytes like working example
|
||||
usage: 0,
|
||||
ocsp_stapling: 3600, // From working example
|
||||
ocsp_stapling: 3600, // From working example
|
||||
one_time_loading: true, // From working example
|
||||
build_chain: false,
|
||||
certificate_path: "".to_string(), // Empty paths since we use content
|
||||
key_path: "".to_string(), // Empty paths since we use content
|
||||
key_path: "".to_string(), // Empty paths since we use content
|
||||
};
|
||||
|
||||
|
||||
// Create TLS config with proper fields like working example
|
||||
let mut tls_config = TlsConfig::default();
|
||||
tls_config.certificate = vec![tls_cert];
|
||||
tls_config.next_protocol = vec!["h2".to_string(), "http/1.1".to_string()]; // From working example
|
||||
tls_config.server_name = "localhost".to_string(); // From working example
|
||||
tls_config.min_version = "1.2".to_string(); // From Marzban examples
|
||||
|
||||
|
||||
// Create TypedMessage for TLS config
|
||||
let tls_message = TypedMessage {
|
||||
r#type: "xray.transport.internet.tls.Config".to_string(),
|
||||
value: tls_config.encode_to_vec(),
|
||||
};
|
||||
|
||||
tracing::debug!("TLS config: server_name={}, protocols={:?}",
|
||||
tls_config.server_name, tls_config.next_protocol);
|
||||
|
||||
|
||||
tracing::debug!(
|
||||
"TLS config: server_name={}, protocols={:?}",
|
||||
tls_config.server_name,
|
||||
tls_config.next_protocol
|
||||
);
|
||||
|
||||
// Create StreamConfig like working example
|
||||
Some(StreamConfig {
|
||||
address: None, // No address in streamSettings according to working example
|
||||
port: 0, // No port in working example streamSettings
|
||||
port: 0, // No port in working example streamSettings
|
||||
protocol_name: "tcp".to_string(),
|
||||
transport_settings: vec![],
|
||||
security_type: "xray.transport.internet.tls.Config".to_string(), // Full type like working example
|
||||
@@ -125,8 +140,8 @@ impl<'a> InboundClient<'a> {
|
||||
|
||||
let receiver_config = ReceiverConfig {
|
||||
port_list: Some(port_list),
|
||||
listen: Some(IpOrDomain {
|
||||
address: Some(Address::Ip(vec![0, 0, 0, 0])) // "0.0.0.0" as IPv4 bytes
|
||||
listen: Some(IpOrDomain {
|
||||
address: Some(Address::Ip(vec![0, 0, 0, 0])), // "0.0.0.0" as IPv4 bytes
|
||||
}),
|
||||
allocation_strategy: None,
|
||||
stream_settings: stream_settings,
|
||||
@@ -138,7 +153,7 @@ impl<'a> InboundClient<'a> {
|
||||
r#type: "xray.app.proxyman.ReceiverConfig".to_string(),
|
||||
value: receiver_config.encode_to_vec(),
|
||||
};
|
||||
|
||||
|
||||
// Create proxy configuration based on protocol with users
|
||||
let proxy_message = match protocol {
|
||||
"vless" => {
|
||||
@@ -148,7 +163,7 @@ impl<'a> InboundClient<'a> {
|
||||
let user_id = user["id"].as_str().unwrap_or("").to_string();
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
|
||||
|
||||
if !user_id.is_empty() && !email.is_empty() {
|
||||
let account = VlessAccount {
|
||||
id: user_id,
|
||||
@@ -166,7 +181,7 @@ impl<'a> InboundClient<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let vless_config = VlessInboundConfig {
|
||||
clients,
|
||||
decryption: "none".to_string(),
|
||||
@@ -176,7 +191,7 @@ impl<'a> InboundClient<'a> {
|
||||
r#type: "xray.proxy.vless.inbound.Config".to_string(),
|
||||
value: vless_config.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
}
|
||||
"vmess" => {
|
||||
let mut vmess_users = vec![];
|
||||
if let Some(users) = users {
|
||||
@@ -184,18 +199,18 @@ impl<'a> InboundClient<'a> {
|
||||
let user_id = user["id"].as_str().unwrap_or("").to_string();
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
|
||||
|
||||
// Validate required fields
|
||||
if user_id.is_empty() || email.is_empty() {
|
||||
tracing::warn!("Skipping VMess user: missing id or email");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Validate UUID format
|
||||
if uuid::Uuid::parse_str(&user_id).is_err() {
|
||||
tracing::warn!("VMess user '{}' has invalid UUID format", user_id);
|
||||
}
|
||||
|
||||
|
||||
if !user_id.is_empty() && !email.is_empty() {
|
||||
let account = VmessAccount {
|
||||
id: user_id.clone(),
|
||||
@@ -203,7 +218,7 @@ impl<'a> InboundClient<'a> {
|
||||
tests_enabled: "".to_string(), // Keep empty as in examples
|
||||
};
|
||||
let account_bytes = account.encode_to_vec();
|
||||
|
||||
|
||||
vmess_users.push(User {
|
||||
email: email.clone(),
|
||||
level,
|
||||
@@ -215,7 +230,7 @@ impl<'a> InboundClient<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let vmess_config = VmessInboundConfig {
|
||||
user: vmess_users,
|
||||
default: None,
|
||||
@@ -225,19 +240,21 @@ impl<'a> InboundClient<'a> {
|
||||
r#type: "xray.proxy.vmess.inbound.Config".to_string(),
|
||||
value: vmess_config.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
}
|
||||
"trojan" => {
|
||||
let mut trojan_users = vec![];
|
||||
if let Some(users) = users {
|
||||
for user in users {
|
||||
let password = user["password"].as_str().or_else(|| user["id"].as_str()).unwrap_or("").to_string();
|
||||
let password = user["password"]
|
||||
.as_str()
|
||||
.or_else(|| user["id"].as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
|
||||
|
||||
if !password.is_empty() && !email.is_empty() {
|
||||
let account = TrojanAccount {
|
||||
password,
|
||||
};
|
||||
let account = TrojanAccount { password };
|
||||
trojan_users.push(User {
|
||||
email,
|
||||
level,
|
||||
@@ -249,7 +266,7 @@ impl<'a> InboundClient<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let trojan_config = TrojanServerConfig {
|
||||
users: trojan_users,
|
||||
fallbacks: vec![],
|
||||
@@ -258,21 +275,24 @@ impl<'a> InboundClient<'a> {
|
||||
r#type: "xray.proxy.trojan.ServerConfig".to_string(),
|
||||
value: trojan_config.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
}
|
||||
"shadowsocks" => {
|
||||
let mut ss_users = vec![];
|
||||
if let Some(users) = users {
|
||||
for user in users {
|
||||
let password = user["password"].as_str().or_else(|| user["id"].as_str()).unwrap_or("").to_string();
|
||||
let password = user["password"]
|
||||
.as_str()
|
||||
.or_else(|| user["id"].as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
|
||||
|
||||
|
||||
if !password.is_empty() && !email.is_empty() {
|
||||
let account = ShadowsocksAccount {
|
||||
password,
|
||||
cipher_type: CipherType::Aes256Gcm as i32, // Use AES-256-GCM cipher
|
||||
iv_check: false, // Default IV check
|
||||
iv_check: false, // Default IV check
|
||||
};
|
||||
ss_users.push(User {
|
||||
email: email.clone(),
|
||||
@@ -285,7 +305,7 @@ impl<'a> InboundClient<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let shadowsocks_config = ShadowsocksServerConfig {
|
||||
users: ss_users,
|
||||
network: vec![Network::Tcp as i32, Network::Udp as i32], // Support TCP and UDP
|
||||
@@ -294,7 +314,7 @@ impl<'a> InboundClient<'a> {
|
||||
r#type: "xray.proxy.shadowsocks.ServerConfig".to_string(),
|
||||
value: shadowsocks_config.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("Unsupported protocol: {}", protocol));
|
||||
}
|
||||
@@ -328,12 +348,12 @@ impl<'a> InboundClient<'a> {
|
||||
let request = Request::new(RemoveInboundRequest {
|
||||
tag: tag.to_string(),
|
||||
});
|
||||
|
||||
|
||||
match handler_client.remove_inbound(request).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Removed inbound '{}' from {}", tag, self.endpoint);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove inbound '{}': {}", tag, e);
|
||||
Err(anyhow!("Failed to remove inbound: {}", e))
|
||||
@@ -342,11 +362,17 @@ impl<'a> InboundClient<'a> {
|
||||
}
|
||||
|
||||
/// Restart Xray with new configuration
|
||||
pub async fn restart_with_config(&self, config: &crate::services::xray::XrayConfig) -> Result<()> {
|
||||
tracing::debug!("Restarting Xray server at {} with new config", self.endpoint);
|
||||
|
||||
pub async fn restart_with_config(
|
||||
&self,
|
||||
config: &crate::services::xray::XrayConfig,
|
||||
) -> Result<()> {
|
||||
tracing::debug!(
|
||||
"Restarting Xray server at {} with new config",
|
||||
self.endpoint
|
||||
);
|
||||
|
||||
// TODO: Implement restart with config using xray-core
|
||||
// For now just return success
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::{Duration, Instant, timeout};
|
||||
use tokio::time::{timeout, Duration, Instant};
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod stats;
|
||||
pub mod inbounds;
|
||||
pub mod stats;
|
||||
pub mod users;
|
||||
|
||||
pub use client::XrayClient;
|
||||
@@ -30,7 +30,7 @@ impl CachedConnection {
|
||||
created_at: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn is_expired(&self, ttl: Duration) -> bool {
|
||||
self.created_at.elapsed() > ttl
|
||||
}
|
||||
@@ -51,7 +51,7 @@ impl XrayService {
|
||||
connection_ttl: Duration::from_secs(300), // 5 minutes TTL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get or create cached client for endpoint
|
||||
async fn get_or_create_client(&self, endpoint: &str) -> Result<XrayClient> {
|
||||
// Check cache first
|
||||
@@ -63,21 +63,20 @@ impl XrayService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create new connection
|
||||
let client = XrayClient::connect(endpoint).await?;
|
||||
let cached_connection = CachedConnection::new(client.clone());
|
||||
|
||||
|
||||
// Update cache
|
||||
{
|
||||
let mut cache = self.connection_cache.write().await;
|
||||
cache.insert(endpoint.to_string(), cached_connection);
|
||||
}
|
||||
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
|
||||
/// Test connection to Xray server with timeout
|
||||
pub async fn test_connection(&self, _server_id: Uuid, endpoint: &str) -> Result<bool> {
|
||||
// Apply a 3-second timeout to the entire test operation
|
||||
@@ -85,12 +84,12 @@ impl XrayService {
|
||||
Ok(Ok(_client)) => {
|
||||
// Connection successful
|
||||
Ok(true)
|
||||
},
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// Connection failed with error
|
||||
warn!("Failed to connect to Xray at {}: {}", endpoint, e);
|
||||
Ok(false)
|
||||
},
|
||||
}
|
||||
Err(_) => {
|
||||
// Operation timed out
|
||||
warn!("Connection test to Xray at {} timed out", endpoint);
|
||||
@@ -100,7 +99,12 @@ impl XrayService {
|
||||
}
|
||||
|
||||
/// Apply full configuration to Xray server
|
||||
pub async fn apply_config(&self, _server_id: Uuid, endpoint: &str, config: &XrayConfig) -> Result<()> {
|
||||
pub async fn apply_config(
|
||||
&self,
|
||||
_server_id: Uuid,
|
||||
endpoint: &str,
|
||||
config: &XrayConfig,
|
||||
) -> Result<()> {
|
||||
let client = self.get_or_create_client(endpoint).await?;
|
||||
client.restart_with_config(config).await
|
||||
}
|
||||
@@ -124,8 +128,9 @@ impl XrayService {
|
||||
"settings": base_settings,
|
||||
"streamSettings": stream_settings
|
||||
});
|
||||
|
||||
self.add_inbound(_server_id, endpoint, &inbound_config).await
|
||||
|
||||
self.add_inbound(_server_id, endpoint, &inbound_config)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create inbound from template with TLS certificate
|
||||
@@ -149,26 +154,51 @@ impl XrayService {
|
||||
"settings": base_settings,
|
||||
"streamSettings": stream_settings
|
||||
});
|
||||
|
||||
self.add_inbound_with_certificate(_server_id, endpoint, &inbound_config, cert_pem, key_pem).await
|
||||
|
||||
self.add_inbound_with_certificate(_server_id, endpoint, &inbound_config, cert_pem, key_pem)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Add inbound to running Xray instance
|
||||
pub async fn add_inbound(&self, _server_id: Uuid, endpoint: &str, inbound: &Value) -> Result<()> {
|
||||
pub async fn add_inbound(
|
||||
&self,
|
||||
_server_id: Uuid,
|
||||
endpoint: &str,
|
||||
inbound: &Value,
|
||||
) -> Result<()> {
|
||||
let client = self.get_or_create_client(endpoint).await?;
|
||||
client.add_inbound(inbound).await
|
||||
}
|
||||
|
||||
/// Add inbound with certificate to running Xray instance
|
||||
pub async fn add_inbound_with_certificate(&self, _server_id: Uuid, endpoint: &str, inbound: &Value, cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
pub async fn add_inbound_with_certificate(
|
||||
&self,
|
||||
_server_id: Uuid,
|
||||
endpoint: &str,
|
||||
inbound: &Value,
|
||||
cert_pem: Option<&str>,
|
||||
key_pem: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let client = self.get_or_create_client(endpoint).await?;
|
||||
client.add_inbound_with_certificate(inbound, cert_pem, key_pem).await
|
||||
client
|
||||
.add_inbound_with_certificate(inbound, cert_pem, key_pem)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Add inbound with users and certificate to running Xray instance
|
||||
pub async fn add_inbound_with_users_and_certificate(&self, _server_id: Uuid, endpoint: &str, inbound: &Value, users: &[Value], cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
pub async fn add_inbound_with_users_and_certificate(
|
||||
&self,
|
||||
_server_id: Uuid,
|
||||
endpoint: &str,
|
||||
inbound: &Value,
|
||||
users: &[Value],
|
||||
cert_pem: Option<&str>,
|
||||
key_pem: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let client = self.get_or_create_client(endpoint).await?;
|
||||
client.add_inbound_with_users_and_certificate(inbound, users, cert_pem, key_pem).await
|
||||
client
|
||||
.add_inbound_with_users_and_certificate(inbound, users, cert_pem, key_pem)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Remove inbound from running Xray instance
|
||||
@@ -178,15 +208,20 @@ impl XrayService {
|
||||
}
|
||||
|
||||
/// Add user to inbound by recreating the inbound with updated user list
|
||||
pub async fn add_user(&self, _server_id: Uuid, endpoint: &str, inbound_tag: &str, user: &Value) -> Result<()> {
|
||||
|
||||
pub async fn add_user(
|
||||
&self,
|
||||
_server_id: Uuid,
|
||||
endpoint: &str,
|
||||
inbound_tag: &str,
|
||||
user: &Value,
|
||||
) -> Result<()> {
|
||||
// TODO: Implement inbound recreation approach:
|
||||
// 1. Get current inbound configuration from database
|
||||
// 2. Get existing users from database
|
||||
// 2. Get existing users from database
|
||||
// 3. Remove old inbound from xray
|
||||
// 4. Create new inbound with all users (existing + new)
|
||||
// For now, return error to indicate this needs to be implemented
|
||||
|
||||
|
||||
Err(anyhow::anyhow!("User addition requires inbound recreation - not yet implemented. Use web interface to recreate inbound with users."))
|
||||
}
|
||||
|
||||
@@ -204,7 +239,6 @@ impl XrayService {
|
||||
cert_pem: Option<&str>,
|
||||
key_pem: Option<&str>,
|
||||
) -> Result<()> {
|
||||
|
||||
// Build inbound configuration with users
|
||||
let mut inbound_config = serde_json::json!({
|
||||
"tag": tag,
|
||||
@@ -213,37 +247,53 @@ impl XrayService {
|
||||
"settings": base_settings,
|
||||
"streamSettings": stream_settings
|
||||
});
|
||||
|
||||
|
||||
// Add users to settings based on protocol
|
||||
if !users.is_empty() {
|
||||
let mut settings = inbound_config["settings"].clone();
|
||||
match protocol {
|
||||
"vless" | "vmess" => {
|
||||
settings["clients"] = serde_json::Value::Array(users.to_vec());
|
||||
},
|
||||
}
|
||||
"trojan" => {
|
||||
settings["clients"] = serde_json::Value::Array(users.to_vec());
|
||||
},
|
||||
}
|
||||
"shadowsocks" => {
|
||||
// For shadowsocks, users are handled differently
|
||||
if let Some(user) = users.first() {
|
||||
settings["password"] = user["password"].clone();
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!("Unsupported protocol for users: {}", protocol));
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unsupported protocol for users: {}",
|
||||
protocol
|
||||
));
|
||||
}
|
||||
}
|
||||
inbound_config["settings"] = settings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Use the new method with users support
|
||||
self.add_inbound_with_users_and_certificate(_server_id, endpoint, &inbound_config, users, cert_pem, key_pem).await
|
||||
self.add_inbound_with_users_and_certificate(
|
||||
_server_id,
|
||||
endpoint,
|
||||
&inbound_config,
|
||||
users,
|
||||
cert_pem,
|
||||
key_pem,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Remove user from inbound
|
||||
pub async fn remove_user(&self, _server_id: Uuid, endpoint: &str, inbound_tag: &str, email: &str) -> Result<()> {
|
||||
pub async fn remove_user(
|
||||
&self,
|
||||
_server_id: Uuid,
|
||||
endpoint: &str,
|
||||
inbound_tag: &str,
|
||||
email: &str,
|
||||
) -> Result<()> {
|
||||
let client = self.get_or_create_client(endpoint).await?;
|
||||
client.remove_user(inbound_tag, email).await
|
||||
}
|
||||
@@ -255,11 +305,17 @@ impl XrayService {
|
||||
}
|
||||
|
||||
/// Query specific statistics
|
||||
pub async fn query_stats(&self, _server_id: Uuid, endpoint: &str, pattern: &str, reset: bool) -> Result<Value> {
|
||||
pub async fn query_stats(
|
||||
&self,
|
||||
_server_id: Uuid,
|
||||
endpoint: &str,
|
||||
pattern: &str,
|
||||
reset: bool,
|
||||
) -> Result<Value> {
|
||||
let client = self.get_or_create_client(endpoint).await?;
|
||||
client.query_stats(pattern, reset).await
|
||||
}
|
||||
|
||||
|
||||
/// Sync entire server with batch operations using single client
|
||||
pub async fn sync_server_inbounds_optimized(
|
||||
&self,
|
||||
@@ -269,21 +325,25 @@ impl XrayService {
|
||||
) -> Result<()> {
|
||||
// Get single client for all operations
|
||||
let client = self.get_or_create_client(endpoint).await?;
|
||||
|
||||
|
||||
// Perform all operations with the same client
|
||||
for (tag, desired) in desired_inbounds {
|
||||
// Always try to remove inbound first (ignore errors if it doesn't exist)
|
||||
let _ = client.remove_inbound(tag).await;
|
||||
|
||||
|
||||
// Create inbound with users
|
||||
let users_json: Vec<Value> = desired.users.iter().map(|user| {
|
||||
serde_json::json!({
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"level": user.level
|
||||
let users_json: Vec<Value> = desired
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| {
|
||||
serde_json::json!({
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"level": user.level
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
|
||||
.collect();
|
||||
|
||||
// Build inbound config
|
||||
let inbound_config = serde_json::json!({
|
||||
"tag": desired.tag,
|
||||
@@ -292,20 +352,23 @@ impl XrayService {
|
||||
"settings": desired.settings,
|
||||
"streamSettings": desired.stream_settings
|
||||
});
|
||||
|
||||
match client.add_inbound_with_users_and_certificate(
|
||||
&inbound_config,
|
||||
&users_json,
|
||||
desired.cert_pem.as_deref(),
|
||||
desired.key_pem.as_deref(),
|
||||
).await {
|
||||
|
||||
match client
|
||||
.add_inbound_with_users_and_certificate(
|
||||
&inbound_config,
|
||||
&users_json,
|
||||
desired.cert_pem.as_deref(),
|
||||
desired.key_pem.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
error!("Failed to create inbound {}: {}", tag, e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -314,4 +377,4 @@ impl Default for XrayService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde_json::Value;
|
||||
use xray_core::{
|
||||
tonic::Request,
|
||||
app::stats::command::{GetStatsRequest, QueryStatsRequest},
|
||||
tonic::Request,
|
||||
Client,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ impl<'a> StatsClient<'a> {
|
||||
/// Get server statistics
|
||||
pub async fn get_stats(&self) -> Result<Value> {
|
||||
tracing::info!("Getting stats from Xray server at {}", self.endpoint);
|
||||
|
||||
|
||||
let request = Request::new(GetStatsRequest {
|
||||
name: "".to_string(),
|
||||
reset: false,
|
||||
@@ -44,8 +44,13 @@ impl<'a> StatsClient<'a> {
|
||||
|
||||
/// Query specific statistics with pattern
|
||||
pub async fn query_stats(&self, pattern: &str, reset: bool) -> Result<Value> {
|
||||
tracing::info!("Querying stats with pattern '{}', reset: {} from {}", pattern, reset, self.endpoint);
|
||||
|
||||
tracing::info!(
|
||||
"Querying stats with pattern '{}', reset: {} from {}",
|
||||
pattern,
|
||||
reset,
|
||||
self.endpoint
|
||||
);
|
||||
|
||||
let request = Request::new(QueryStatsRequest {
|
||||
pattern: pattern.to_string(),
|
||||
reset,
|
||||
@@ -67,4 +72,4 @@ impl<'a> StatsClient<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{anyhow, Result};
|
||||
use prost::Message;
|
||||
use serde_json::Value;
|
||||
use xray_core::{
|
||||
tonic::Request,
|
||||
app::proxyman::command::{AlterInboundRequest, AddUserOperation, RemoveUserOperation},
|
||||
common::serial::TypedMessage,
|
||||
app::proxyman::command::{AddUserOperation, AlterInboundRequest, RemoveUserOperation},
|
||||
common::protocol::User,
|
||||
common::serial::TypedMessage,
|
||||
proxy::trojan::Account as TrojanAccount,
|
||||
proxy::vless::Account as VlessAccount,
|
||||
proxy::vmess::Account as VmessAccount,
|
||||
proxy::trojan::Account as TrojanAccount,
|
||||
tonic::Request,
|
||||
Client,
|
||||
};
|
||||
use prost::Message;
|
||||
|
||||
pub struct UserClient<'a> {
|
||||
endpoint: String,
|
||||
@@ -28,11 +28,11 @@ impl<'a> UserClient<'a> {
|
||||
let user_id = user["id"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
let protocol = user["protocol"].as_str().unwrap_or("vless");
|
||||
|
||||
|
||||
if email.is_empty() || user_id.is_empty() {
|
||||
return Err(anyhow!("User email and id are required"));
|
||||
}
|
||||
|
||||
|
||||
// Create user account based on protocol
|
||||
let account_message = match protocol {
|
||||
"vless" => {
|
||||
@@ -45,7 +45,7 @@ impl<'a> UserClient<'a> {
|
||||
r#type: "xray.proxy.vless.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
}
|
||||
"vmess" => {
|
||||
let account = VmessAccount {
|
||||
id: user_id,
|
||||
@@ -56,7 +56,7 @@ impl<'a> UserClient<'a> {
|
||||
r#type: "xray.proxy.vmess.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
}
|
||||
"trojan" => {
|
||||
let account = TrojanAccount {
|
||||
password: user_id, // For trojan, use password instead of UUID
|
||||
@@ -65,36 +65,35 @@ impl<'a> UserClient<'a> {
|
||||
r#type: "xray.proxy.trojan.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("Unsupported protocol for user: {}", protocol));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create user protobuf message
|
||||
let user_proto = User {
|
||||
level: level,
|
||||
email: email.clone(),
|
||||
account: Some(account_message),
|
||||
};
|
||||
|
||||
|
||||
// Build the AddUserOperation
|
||||
let add_user_op = AddUserOperation {
|
||||
user: Some(user_proto),
|
||||
};
|
||||
|
||||
|
||||
let typed_message = TypedMessage {
|
||||
r#type: "xray.app.proxyman.command.AddUserOperation".to_string(),
|
||||
value: add_user_op.encode_to_vec(),
|
||||
};
|
||||
|
||||
|
||||
// Build the AlterInboundRequest
|
||||
let request = Request::new(AlterInboundRequest {
|
||||
tag: inbound_tag.to_string(),
|
||||
operation: Some(typed_message),
|
||||
});
|
||||
|
||||
|
||||
|
||||
let mut handler_client = self.client.handler();
|
||||
match handler_client.alter_inbound(request).await {
|
||||
Ok(response) => {
|
||||
@@ -102,40 +101,57 @@ impl<'a> UserClient<'a> {
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("gRPC error adding user '{}' to inbound '{}': status={}, message={}",
|
||||
email, inbound_tag, e.code(), e.message());
|
||||
Err(anyhow!("Failed to add user '{}' to inbound '{}': {}", email, inbound_tag, e))
|
||||
tracing::error!(
|
||||
"gRPC error adding user '{}' to inbound '{}': status={}, message={}",
|
||||
email,
|
||||
inbound_tag,
|
||||
e.code(),
|
||||
e.message()
|
||||
);
|
||||
Err(anyhow!(
|
||||
"Failed to add user '{}' to inbound '{}': {}",
|
||||
email,
|
||||
inbound_tag,
|
||||
e
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove user from inbound
|
||||
pub async fn remove_user(&self, inbound_tag: &str, email: &str) -> Result<()> {
|
||||
|
||||
// Build the RemoveUserOperation
|
||||
let remove_user_op = RemoveUserOperation {
|
||||
email: email.to_string(),
|
||||
};
|
||||
|
||||
|
||||
let typed_message = TypedMessage {
|
||||
r#type: "xray.app.proxyman.command.RemoveUserOperation".to_string(),
|
||||
value: remove_user_op.encode_to_vec(),
|
||||
};
|
||||
|
||||
|
||||
let request = Request::new(AlterInboundRequest {
|
||||
tag: inbound_tag.to_string(),
|
||||
operation: Some(typed_message),
|
||||
});
|
||||
|
||||
|
||||
let mut handler_client = self.client.handler();
|
||||
match handler_client.alter_inbound(request).await {
|
||||
Ok(_) => {
|
||||
Ok(())
|
||||
}
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove user '{}' from inbound '{}': {}", email, inbound_tag, e);
|
||||
Err(anyhow!("Failed to remove user '{}' from inbound '{}': {}", email, inbound_tag, e))
|
||||
tracing::error!(
|
||||
"Failed to remove user '{}' from inbound '{}': {}",
|
||||
email,
|
||||
inbound_tag,
|
||||
e
|
||||
);
|
||||
Err(anyhow!(
|
||||
"Failed to remove user '{}' from inbound '{}': {}",
|
||||
email,
|
||||
inbound_tag,
|
||||
e
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
use crate::{
|
||||
database::{entities::certificate, repository::CertificateRepository},
|
||||
services::certificates::CertificateService,
|
||||
web::AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
@@ -6,27 +11,17 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
use crate::{
|
||||
database::{
|
||||
entities::certificate,
|
||||
repository::CertificateRepository,
|
||||
},
|
||||
services::certificates::CertificateService,
|
||||
web::AppState,
|
||||
};
|
||||
|
||||
/// List all certificates
|
||||
pub async fn list_certificates(
|
||||
State(app_state): State<AppState>,
|
||||
) -> Result<Json<Vec<certificate::CertificateResponse>>, StatusCode> {
|
||||
let repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_all().await {
|
||||
Ok(certificates) => {
|
||||
let responses: Vec<certificate::CertificateResponse> = certificates
|
||||
.into_iter()
|
||||
.map(|c| c.into())
|
||||
.collect();
|
||||
let responses: Vec<certificate::CertificateResponse> =
|
||||
certificates.into_iter().map(|c| c.into()).collect();
|
||||
Ok(Json(responses))
|
||||
}
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -39,7 +34,7 @@ pub async fn get_certificate(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<certificate::CertificateResponse>, StatusCode> {
|
||||
let repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_by_id(id).await {
|
||||
Ok(Some(certificate)) => Ok(Json(certificate.into())),
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -53,7 +48,7 @@ pub async fn get_certificate_details(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<certificate::CertificateDetailsResponse>, StatusCode> {
|
||||
let repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_by_id(id).await {
|
||||
Ok(Some(certificate)) => Ok(Json(certificate.into())),
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -69,74 +64,99 @@ pub async fn create_certificate(
|
||||
tracing::info!("Creating certificate: {:?}", cert_data);
|
||||
let repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
let cert_service = CertificateService::new();
|
||||
|
||||
|
||||
// Generate certificate based on type
|
||||
let (cert_pem, private_key) = match cert_data.cert_type.as_str() {
|
||||
"self_signed" => {
|
||||
cert_service.generate_self_signed(&cert_data.domain).await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to generate self-signed certificate: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"self_signed" => cert_service
|
||||
.generate_self_signed(&cert_data.domain)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to generate self-signed certificate: {:?}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": "Failed to generate self-signed certificate",
|
||||
"details": format!("{:?}", e)
|
||||
})))
|
||||
})?
|
||||
}
|
||||
})),
|
||||
)
|
||||
})?,
|
||||
"letsencrypt" => {
|
||||
// Validate required fields for Let's Encrypt
|
||||
let dns_provider_id = cert_data.dns_provider_id
|
||||
.ok_or((StatusCode::BAD_REQUEST, Json(json!({
|
||||
let dns_provider_id = cert_data.dns_provider_id.ok_or((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": "DNS provider ID is required for Let's Encrypt certificates"
|
||||
}))))?;
|
||||
let acme_email = cert_data.acme_email
|
||||
.as_ref()
|
||||
.ok_or((StatusCode::BAD_REQUEST, Json(json!({
|
||||
})),
|
||||
))?;
|
||||
let acme_email = cert_data.acme_email.as_ref().ok_or((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": "ACME email is required for Let's Encrypt certificates"
|
||||
}))))?;
|
||||
|
||||
})),
|
||||
))?;
|
||||
|
||||
let cert_service = CertificateService::with_db(app_state.db.connection().clone());
|
||||
cert_service.generate_letsencrypt_certificate(
|
||||
&cert_data.domain,
|
||||
dns_provider_id,
|
||||
acme_email,
|
||||
false // production by default
|
||||
).await
|
||||
cert_service
|
||||
.generate_letsencrypt_certificate(
|
||||
&cert_data.domain,
|
||||
dns_provider_id,
|
||||
acme_email,
|
||||
false, // production by default
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to generate Let's Encrypt certificate: {:?}", e);
|
||||
// Return a more detailed error response
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": "Failed to generate Let's Encrypt certificate",
|
||||
"details": format!("{:?}", e)
|
||||
})))
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": "Failed to generate Let's Encrypt certificate",
|
||||
"details": format!("{:?}", e)
|
||||
})),
|
||||
)
|
||||
})?
|
||||
}
|
||||
"imported" => {
|
||||
// For imported certificates, use provided PEM data
|
||||
if cert_data.certificate_pem.is_empty() || cert_data.private_key.is_empty() {
|
||||
return Err((StatusCode::BAD_REQUEST, Json(json!({
|
||||
"error": "Certificate PEM and private key are required for imported certificates"
|
||||
}))));
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": "Certificate PEM and private key are required for imported certificates"
|
||||
})),
|
||||
));
|
||||
}
|
||||
(cert_data.certificate_pem.clone(), cert_data.private_key.clone())
|
||||
(
|
||||
cert_data.certificate_pem.clone(),
|
||||
cert_data.private_key.clone(),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": "Invalid certificate type. Supported types: self_signed, letsencrypt, imported"
|
||||
})),
|
||||
))
|
||||
}
|
||||
_ => return Err((StatusCode::BAD_REQUEST, Json(json!({
|
||||
"error": "Invalid certificate type. Supported types: self_signed, letsencrypt, imported"
|
||||
})))),
|
||||
};
|
||||
|
||||
|
||||
// Create certificate with generated data
|
||||
let mut create_dto = cert_data;
|
||||
create_dto.certificate_pem = cert_pem;
|
||||
create_dto.private_key = private_key;
|
||||
|
||||
|
||||
match repo.create(create_dto).await {
|
||||
Ok(certificate) => Ok(Json(certificate.into())),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to save certificate to database: {:?}", e);
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": "Failed to save certificate to database",
|
||||
"details": format!("{:?}", e)
|
||||
}))))
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": "Failed to save certificate to database",
|
||||
"details": format!("{:?}", e)
|
||||
})),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +168,7 @@ pub async fn update_certificate(
|
||||
JsonExtractor(cert_data): JsonExtractor<certificate::UpdateCertificateDto>,
|
||||
) -> Result<Json<certificate::CertificateResponse>, StatusCode> {
|
||||
let repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.update(id, cert_data).await {
|
||||
Ok(certificate) => Ok(Json(certificate.into())),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -161,7 +181,7 @@ pub async fn delete_certificate(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.delete(id).await {
|
||||
Ok(true) => Ok(StatusCode::NO_CONTENT),
|
||||
Ok(false) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -174,16 +194,14 @@ pub async fn get_expiring_certificates(
|
||||
State(app_state): State<AppState>,
|
||||
) -> Result<Json<Vec<certificate::CertificateResponse>>, StatusCode> {
|
||||
let repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Get certificates expiring in next 30 days
|
||||
match repo.find_expiring_soon(30).await {
|
||||
Ok(certificates) => {
|
||||
let responses: Vec<certificate::CertificateResponse> = certificates
|
||||
.into_iter()
|
||||
.map(|c| c.into())
|
||||
.collect();
|
||||
let responses: Vec<certificate::CertificateResponse> =
|
||||
certificates.into_iter().map(|c| c.into()).collect();
|
||||
Ok(Json(responses))
|
||||
}
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,18 +34,20 @@ pub async fn get_user_inbound_config(
|
||||
) -> Result<Json<ClientConfigResponse>, StatusCode> {
|
||||
let repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
let uri_service = UriGeneratorService::new();
|
||||
|
||||
|
||||
// Get client configuration data
|
||||
let config_data = repo.get_client_config_data(user_id, inbound_id)
|
||||
let config_data = repo
|
||||
.get_client_config_data(user_id, inbound_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
let config_data = config_data.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
|
||||
// Generate URI
|
||||
let client_config = uri_service.generate_client_config(user_id, &config_data)
|
||||
let client_config = uri_service
|
||||
.generate_client_config(user_id, &config_data)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
let response = ClientConfigResponse {
|
||||
user_id: client_config.user_id,
|
||||
server_name: client_config.server_name,
|
||||
@@ -54,7 +56,7 @@ pub async fn get_user_inbound_config(
|
||||
uri: client_config.uri,
|
||||
qr_code: client_config.qr_code,
|
||||
};
|
||||
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
@@ -65,14 +67,15 @@ pub async fn get_user_configs(
|
||||
) -> Result<Json<Vec<ClientConfigResponse>>, StatusCode> {
|
||||
let repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
let uri_service = UriGeneratorService::new();
|
||||
|
||||
|
||||
// Get all client configuration data for user
|
||||
let configs_data = repo.get_all_client_configs_for_user(user_id)
|
||||
let configs_data = repo
|
||||
.get_all_client_configs_for_user(user_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
let mut responses = Vec::new();
|
||||
|
||||
|
||||
for config_data in configs_data {
|
||||
match uri_service.generate_client_config(user_id, &config_data) {
|
||||
Ok(client_config) => {
|
||||
@@ -84,14 +87,14 @@ pub async fn get_user_configs(
|
||||
uri: client_config.uri,
|
||||
qr_code: client_config.qr_code,
|
||||
});
|
||||
},
|
||||
}
|
||||
Err(_) => {
|
||||
// Log error but continue with other configs
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(Json(responses))
|
||||
}
|
||||
|
||||
@@ -102,17 +105,21 @@ pub async fn get_inbound_configs(
|
||||
) -> Result<Json<Vec<ClientConfigResponse>>, StatusCode> {
|
||||
let repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
let uri_service = UriGeneratorService::new();
|
||||
|
||||
|
||||
// Get all users for this inbound
|
||||
let inbound_users = repo.find_active_by_inbound_id(inbound_id)
|
||||
let inbound_users = repo
|
||||
.find_active_by_inbound_id(inbound_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
let mut responses = Vec::new();
|
||||
|
||||
|
||||
for inbound_user in inbound_users {
|
||||
// Get client configuration data for each user
|
||||
if let Ok(Some(config_data)) = repo.get_client_config_data(inbound_user.user_id, inbound_id).await {
|
||||
if let Ok(Some(config_data)) = repo
|
||||
.get_client_config_data(inbound_user.user_id, inbound_id)
|
||||
.await
|
||||
{
|
||||
match uri_service.generate_client_config(inbound_user.user_id, &config_data) {
|
||||
Ok(client_config) => {
|
||||
responses.push(ClientConfigResponse {
|
||||
@@ -123,7 +130,7 @@ pub async fn get_inbound_configs(
|
||||
uri: client_config.uri,
|
||||
qr_code: client_config.qr_code,
|
||||
});
|
||||
},
|
||||
}
|
||||
Err(_) => {
|
||||
// Log error but continue with other configs
|
||||
continue;
|
||||
@@ -131,6 +138,6 @@ pub async fn get_inbound_configs(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(Json(responses))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
database::{
|
||||
entities::dns_provider::{
|
||||
CreateDnsProviderDto, UpdateDnsProviderDto, DnsProviderResponseDto,
|
||||
CreateDnsProviderDto, DnsProviderResponseDto, UpdateDnsProviderDto,
|
||||
},
|
||||
repository::DnsProviderRepository,
|
||||
},
|
||||
@@ -20,7 +20,7 @@ pub async fn create_dns_provider(
|
||||
Json(dto): Json<CreateDnsProviderDto>,
|
||||
) -> Result<Json<DnsProviderResponseDto>, StatusCode> {
|
||||
let repo = DnsProviderRepository::new(state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.create(dto).await {
|
||||
Ok(provider) => Ok(Json(provider.to_response_dto())),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -31,13 +31,11 @@ pub async fn list_dns_providers(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<DnsProviderResponseDto>>, StatusCode> {
|
||||
let repo = DnsProviderRepository::new(state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_all().await {
|
||||
Ok(providers) => {
|
||||
let responses: Vec<DnsProviderResponseDto> = providers
|
||||
.into_iter()
|
||||
.map(|p| p.to_response_dto())
|
||||
.collect();
|
||||
let responses: Vec<DnsProviderResponseDto> =
|
||||
providers.into_iter().map(|p| p.to_response_dto()).collect();
|
||||
Ok(Json(responses))
|
||||
}
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -49,7 +47,7 @@ pub async fn get_dns_provider(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<DnsProviderResponseDto>, StatusCode> {
|
||||
let repo = DnsProviderRepository::new(state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_by_id(id).await {
|
||||
Ok(Some(provider)) => Ok(Json(provider.to_response_dto())),
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -63,7 +61,7 @@ pub async fn update_dns_provider(
|
||||
Json(dto): Json<UpdateDnsProviderDto>,
|
||||
) -> Result<Json<DnsProviderResponseDto>, StatusCode> {
|
||||
let repo = DnsProviderRepository::new(state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.update(id, dto).await {
|
||||
Ok(Some(updated_provider)) => Ok(Json(updated_provider.to_response_dto())),
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -76,7 +74,7 @@ pub async fn delete_dns_provider(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let repo = DnsProviderRepository::new(state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.delete(id).await {
|
||||
Ok(true) => Ok(StatusCode::NO_CONTENT),
|
||||
Ok(false) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -88,15 +86,13 @@ pub async fn list_active_cloudflare_providers(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<DnsProviderResponseDto>>, StatusCode> {
|
||||
let repo = DnsProviderRepository::new(state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_active_by_type("cloudflare").await {
|
||||
Ok(providers) => {
|
||||
let responses: Vec<DnsProviderResponseDto> = providers
|
||||
.into_iter()
|
||||
.map(|p| p.to_response_dto())
|
||||
.collect();
|
||||
let responses: Vec<DnsProviderResponseDto> =
|
||||
providers.into_iter().map(|p| p.to_response_dto()).collect();
|
||||
Ok(Json(responses))
|
||||
}
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
pub mod users;
|
||||
pub mod servers;
|
||||
pub mod certificates;
|
||||
pub mod templates;
|
||||
pub mod client_configs;
|
||||
pub mod dns_providers;
|
||||
pub mod servers;
|
||||
pub mod subscription;
|
||||
pub mod tasks;
|
||||
pub mod telegram;
|
||||
pub mod templates;
|
||||
pub mod user_requests;
|
||||
pub mod subscription;
|
||||
pub mod users;
|
||||
|
||||
pub use users::*;
|
||||
pub use servers::*;
|
||||
pub use certificates::*;
|
||||
pub use templates::*;
|
||||
pub use client_configs::*;
|
||||
pub use dns_providers::*;
|
||||
pub use servers::*;
|
||||
pub use subscription::*;
|
||||
pub use tasks::*;
|
||||
pub use telegram::*;
|
||||
pub use templates::*;
|
||||
pub use user_requests::*;
|
||||
pub use subscription::*;
|
||||
pub use users::*;
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
use crate::{
|
||||
database::{
|
||||
entities::{server, server_inbound},
|
||||
repository::{
|
||||
CertificateRepository, InboundTemplateRepository, InboundUsersRepository,
|
||||
ServerInboundRepository, ServerRepository, UserRepository,
|
||||
},
|
||||
},
|
||||
web::AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
@@ -5,26 +15,17 @@ use axum::{
|
||||
Json as JsonExtractor,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use crate::{
|
||||
database::{
|
||||
entities::{server, server_inbound},
|
||||
repository::{ServerRepository, ServerInboundRepository, InboundTemplateRepository, CertificateRepository, InboundUsersRepository, UserRepository},
|
||||
},
|
||||
web::AppState,
|
||||
};
|
||||
|
||||
/// List all servers
|
||||
pub async fn list_servers(
|
||||
State(app_state): State<AppState>,
|
||||
) -> Result<Json<Vec<server::ServerResponse>>, StatusCode> {
|
||||
let repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_all().await {
|
||||
Ok(servers) => {
|
||||
let responses: Vec<server::ServerResponse> = servers
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect();
|
||||
let responses: Vec<server::ServerResponse> =
|
||||
servers.into_iter().map(|s| s.into()).collect();
|
||||
Ok(Json(responses))
|
||||
}
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -37,7 +38,7 @@ pub async fn get_server(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<server::ServerResponse>, StatusCode> {
|
||||
let repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_by_id(id).await {
|
||||
Ok(Some(server)) => Ok(Json(server.into())),
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -51,7 +52,7 @@ pub async fn create_server(
|
||||
Json(server_data): Json<server::CreateServerDto>,
|
||||
) -> Result<Json<server::ServerResponse>, StatusCode> {
|
||||
let repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.create(server_data).await {
|
||||
Ok(server) => Ok(Json(server.into())),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -65,7 +66,7 @@ pub async fn update_server(
|
||||
Json(server_data): Json<server::UpdateServerDto>,
|
||||
) -> Result<Json<server::ServerResponse>, StatusCode> {
|
||||
let repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.update(id, server_data).await {
|
||||
Ok(server) => Ok(Json(server.into())),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -78,7 +79,7 @@ pub async fn delete_server(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.delete(id).await {
|
||||
Ok(true) => Ok(StatusCode::NO_CONTENT),
|
||||
Ok(false) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -92,7 +93,7 @@ pub async fn test_server_connection(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
let repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
let server = match repo.find_by_id(id).await {
|
||||
Ok(Some(server)) => server,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
@@ -100,7 +101,7 @@ pub async fn test_server_connection(
|
||||
};
|
||||
|
||||
let endpoint = server.get_grpc_endpoint();
|
||||
|
||||
|
||||
match app_state.xray_service.test_connection(id, &endpoint).await {
|
||||
Ok(connected) => {
|
||||
// Update server status based on connection test
|
||||
@@ -114,14 +115,14 @@ pub async fn test_server_connection(
|
||||
default_certificate_id: None,
|
||||
status: Some(new_status.to_string()),
|
||||
};
|
||||
|
||||
|
||||
let _ = repo.update(id, update_dto).await; // Ignore update errors for now
|
||||
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"connected": connected,
|
||||
"endpoint": endpoint
|
||||
})))
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// Update status to error
|
||||
let update_dto = server::UpdateServerDto {
|
||||
@@ -133,15 +134,15 @@ pub async fn test_server_connection(
|
||||
default_certificate_id: None,
|
||||
status: Some("error".to_string()),
|
||||
};
|
||||
|
||||
|
||||
let _ = repo.update(id, update_dto).await; // Ignore update errors for now
|
||||
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"connected": false,
|
||||
"endpoint": endpoint,
|
||||
"error": e.to_string()
|
||||
})))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ pub async fn get_server_stats(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
let repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
let server = match repo.find_by_id(id).await {
|
||||
Ok(Some(server)) => server,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
@@ -159,7 +160,7 @@ pub async fn get_server_stats(
|
||||
};
|
||||
|
||||
let endpoint = server.get_grpc_endpoint();
|
||||
|
||||
|
||||
match app_state.xray_service.get_stats(id, &endpoint).await {
|
||||
Ok(stats) => Ok(Json(stats)),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -172,7 +173,7 @@ pub async fn list_server_inbounds(
|
||||
Path(server_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<server_inbound::ServerInboundResponse>>, StatusCode> {
|
||||
let repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_by_server_id_with_template(server_id).await {
|
||||
Ok(responses) => Ok(Json(responses)),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -186,51 +187,52 @@ pub async fn create_server_inbound(
|
||||
JsonExtractor(inbound_data): JsonExtractor<server_inbound::CreateServerInboundDto>,
|
||||
) -> Result<Json<server_inbound::ServerInboundResponse>, StatusCode> {
|
||||
tracing::debug!("Creating server inbound for server {}", server_id);
|
||||
|
||||
|
||||
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||
let template_repo = InboundTemplateRepository::new(app_state.db.connection().clone());
|
||||
let cert_repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Get server info
|
||||
let server = match server_repo.find_by_id(server_id).await {
|
||||
Ok(Some(server)) => server,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Get template info
|
||||
let template = match template_repo.find_by_id(inbound_data.template_id).await {
|
||||
Ok(Some(template)) => template,
|
||||
Ok(None) => return Err(StatusCode::BAD_REQUEST),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Create inbound in database first with protocol-aware tag
|
||||
let inbound = match inbound_repo.create_with_protocol(server_id, inbound_data, &template.protocol).await {
|
||||
let inbound = match inbound_repo
|
||||
.create_with_protocol(server_id, inbound_data, &template.protocol)
|
||||
.await
|
||||
{
|
||||
Ok(inbound) => {
|
||||
// Send sync event for immediate synchronization
|
||||
crate::services::events::send_sync_event(
|
||||
crate::services::events::SyncEvent::InboundChanged(server_id)
|
||||
crate::services::events::SyncEvent::InboundChanged(server_id),
|
||||
);
|
||||
inbound
|
||||
},
|
||||
}
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Try to create inbound on xray server only if it's active
|
||||
let endpoint = server.get_grpc_endpoint();
|
||||
if inbound.is_active {
|
||||
// Get certificate data if certificate is specified
|
||||
let (cert_pem, key_pem) = if let Some(cert_id) = inbound.certificate_id {
|
||||
match cert_repo.find_by_id(cert_id).await {
|
||||
Ok(Some(cert)) => {
|
||||
(Some(cert.certificate_pem()), Some(cert.private_key_pem()))
|
||||
},
|
||||
Ok(Some(cert)) => (Some(cert.certificate_pem()), Some(cert.private_key_pem())),
|
||||
Ok(None) => {
|
||||
tracing::warn!("Certificate {} not found", cert_id);
|
||||
(None, None)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error fetching certificate {}: {}", cert_id, e);
|
||||
(None, None)
|
||||
@@ -240,22 +242,31 @@ pub async fn create_server_inbound(
|
||||
(None, None)
|
||||
};
|
||||
|
||||
match app_state.xray_service.create_inbound_with_certificate(
|
||||
server_id,
|
||||
&endpoint,
|
||||
&inbound.tag,
|
||||
inbound.port_override.unwrap_or(template.default_port),
|
||||
&template.protocol,
|
||||
template.base_settings.clone(),
|
||||
template.stream_settings.clone(),
|
||||
cert_pem.as_deref(),
|
||||
key_pem.as_deref(),
|
||||
).await {
|
||||
match app_state
|
||||
.xray_service
|
||||
.create_inbound_with_certificate(
|
||||
server_id,
|
||||
&endpoint,
|
||||
&inbound.tag,
|
||||
inbound.port_override.unwrap_or(template.default_port),
|
||||
&template.protocol,
|
||||
template.base_settings.clone(),
|
||||
template.stream_settings.clone(),
|
||||
cert_pem.as_deref(),
|
||||
key_pem.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!("Created inbound '{}' on {}", inbound.tag, endpoint);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create inbound '{}' on {}: {}", inbound.tag, endpoint, e);
|
||||
tracing::error!(
|
||||
"Failed to create inbound '{}' on {}: {}",
|
||||
inbound.tag,
|
||||
endpoint,
|
||||
e
|
||||
);
|
||||
// Note: We don't fail the request since the inbound is already in DB
|
||||
// The user can manually sync or retry later
|
||||
}
|
||||
@@ -263,7 +274,7 @@ pub async fn create_server_inbound(
|
||||
} else {
|
||||
tracing::debug!("Inbound '{}' created as inactive", inbound.tag);
|
||||
}
|
||||
|
||||
|
||||
Ok(Json(inbound.into()))
|
||||
}
|
||||
|
||||
@@ -273,20 +284,24 @@ pub async fn update_server_inbound(
|
||||
Path((server_id, inbound_id)): Path<(Uuid, Uuid)>,
|
||||
JsonExtractor(inbound_data): JsonExtractor<server_inbound::UpdateServerInboundDto>,
|
||||
) -> Result<Json<server_inbound::ServerInboundResponse>, StatusCode> {
|
||||
tracing::debug!("Updating server inbound {} for server {}", inbound_id, server_id);
|
||||
|
||||
tracing::debug!(
|
||||
"Updating server inbound {} for server {}",
|
||||
inbound_id,
|
||||
server_id
|
||||
);
|
||||
|
||||
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||
let template_repo = InboundTemplateRepository::new(app_state.db.connection().clone());
|
||||
let cert_repo = CertificateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Get server info
|
||||
let server = match server_repo.find_by_id(server_id).await {
|
||||
Ok(Some(server)) => server,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Get current inbound state
|
||||
let current_inbound = match inbound_repo.find_by_id(inbound_id).await {
|
||||
Ok(Some(inbound)) if inbound.server_id == server_id => inbound,
|
||||
@@ -294,48 +309,64 @@ pub async fn update_server_inbound(
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Check if is_active status is changing
|
||||
let old_is_active = current_inbound.is_active;
|
||||
let new_is_active = inbound_data.is_active.unwrap_or(old_is_active);
|
||||
let endpoint = server.get_grpc_endpoint();
|
||||
|
||||
|
||||
// Handle xray server changes based on active status change
|
||||
if old_is_active && !new_is_active {
|
||||
// Becoming inactive - remove from xray server
|
||||
match app_state.xray_service.remove_inbound(server_id, &endpoint, ¤t_inbound.tag).await {
|
||||
match app_state
|
||||
.xray_service
|
||||
.remove_inbound(server_id, &endpoint, ¤t_inbound.tag)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!("Deactivated inbound '{}' on {}", current_inbound.tag, endpoint);
|
||||
},
|
||||
tracing::info!(
|
||||
"Deactivated inbound '{}' on {}",
|
||||
current_inbound.tag,
|
||||
endpoint
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to deactivate inbound '{}': {}", current_inbound.tag, e);
|
||||
tracing::error!(
|
||||
"Failed to deactivate inbound '{}': {}",
|
||||
current_inbound.tag,
|
||||
e
|
||||
);
|
||||
// Continue with database update even if xray removal fails
|
||||
}
|
||||
}
|
||||
} else if !old_is_active && new_is_active {
|
||||
// Becoming active - add to xray server
|
||||
|
||||
|
||||
// Get template info for recreation
|
||||
let template = match template_repo.find_by_id(current_inbound.template_id).await {
|
||||
Ok(Some(template)) => template,
|
||||
Ok(None) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Use updated port if provided, otherwise keep current
|
||||
let port = inbound_data.port_override.unwrap_or(current_inbound.port_override.unwrap_or(template.default_port));
|
||||
|
||||
let port = inbound_data.port_override.unwrap_or(
|
||||
current_inbound
|
||||
.port_override
|
||||
.unwrap_or(template.default_port),
|
||||
);
|
||||
|
||||
// Get certificate data if certificate is specified (could be updated)
|
||||
let certificate_id = inbound_data.certificate_id.or(current_inbound.certificate_id);
|
||||
let certificate_id = inbound_data
|
||||
.certificate_id
|
||||
.or(current_inbound.certificate_id);
|
||||
let (cert_pem, key_pem) = if let Some(cert_id) = certificate_id {
|
||||
match cert_repo.find_by_id(cert_id).await {
|
||||
Ok(Some(cert)) => {
|
||||
(Some(cert.certificate_pem()), Some(cert.private_key_pem()))
|
||||
},
|
||||
Ok(Some(cert)) => (Some(cert.certificate_pem()), Some(cert.private_key_pem())),
|
||||
Ok(None) => {
|
||||
tracing::warn!("Certificate {} not found", cert_id);
|
||||
(None, None)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error fetching certificate {}: {}", cert_id, e);
|
||||
(None, None)
|
||||
@@ -344,37 +375,49 @@ pub async fn update_server_inbound(
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
match app_state.xray_service.create_inbound_with_certificate(
|
||||
server_id,
|
||||
&endpoint,
|
||||
¤t_inbound.tag,
|
||||
port,
|
||||
&template.protocol,
|
||||
template.base_settings.clone(),
|
||||
template.stream_settings.clone(),
|
||||
cert_pem.as_deref(),
|
||||
key_pem.as_deref(),
|
||||
).await {
|
||||
|
||||
match app_state
|
||||
.xray_service
|
||||
.create_inbound_with_certificate(
|
||||
server_id,
|
||||
&endpoint,
|
||||
¤t_inbound.tag,
|
||||
port,
|
||||
&template.protocol,
|
||||
template.base_settings.clone(),
|
||||
template.stream_settings.clone(),
|
||||
cert_pem.as_deref(),
|
||||
key_pem.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!("Activated inbound '{}' on {}", current_inbound.tag, endpoint);
|
||||
},
|
||||
tracing::info!(
|
||||
"Activated inbound '{}' on {}",
|
||||
current_inbound.tag,
|
||||
endpoint
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to activate inbound '{}': {}", current_inbound.tag, e);
|
||||
tracing::error!(
|
||||
"Failed to activate inbound '{}': {}",
|
||||
current_inbound.tag,
|
||||
e
|
||||
);
|
||||
// Continue with database update even if xray creation fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update database
|
||||
match inbound_repo.update(inbound_id, inbound_data).await {
|
||||
Ok(updated_inbound) => {
|
||||
// Send sync event for immediate synchronization
|
||||
crate::services::events::send_sync_event(
|
||||
crate::services::events::SyncEvent::InboundChanged(server_id)
|
||||
crate::services::events::SyncEvent::InboundChanged(server_id),
|
||||
);
|
||||
Ok(Json(updated_inbound.into()))
|
||||
},
|
||||
}
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
@@ -385,12 +428,10 @@ pub async fn get_server_inbound(
|
||||
Path((server_id, inbound_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<server_inbound::ServerInboundResponse>, StatusCode> {
|
||||
let repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Verify the inbound belongs to the server
|
||||
match repo.find_by_id(inbound_id).await {
|
||||
Ok(Some(inbound)) if inbound.server_id == server_id => {
|
||||
Ok(Json(inbound.into()))
|
||||
}
|
||||
Ok(Some(inbound)) if inbound.server_id == server_id => Ok(Json(inbound.into())),
|
||||
Ok(Some(_)) => Err(StatusCode::BAD_REQUEST),
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -404,14 +445,14 @@ pub async fn delete_server_inbound(
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Get server and inbound info
|
||||
let server = match server_repo.find_by_id(server_id).await {
|
||||
Ok(Some(server)) => server,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Verify the inbound belongs to the server
|
||||
let inbound = match inbound_repo.find_by_id(inbound_id).await {
|
||||
Ok(Some(inbound)) if inbound.server_id == server_id => inbound,
|
||||
@@ -419,28 +460,37 @@ pub async fn delete_server_inbound(
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Try to remove inbound from xray server first
|
||||
let endpoint = server.get_grpc_endpoint();
|
||||
match app_state.xray_service.remove_inbound(server_id, &endpoint, &inbound.tag).await {
|
||||
match app_state
|
||||
.xray_service
|
||||
.remove_inbound(server_id, &endpoint, &inbound.tag)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!("Removed inbound '{}' from {}", inbound.tag, endpoint);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove inbound '{}' from {}: {}", inbound.tag, endpoint, e);
|
||||
tracing::error!(
|
||||
"Failed to remove inbound '{}' from {}: {}",
|
||||
inbound.tag,
|
||||
endpoint,
|
||||
e
|
||||
);
|
||||
// Continue with database deletion even if xray removal fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Delete from database
|
||||
match inbound_repo.delete(inbound_id).await {
|
||||
Ok(true) => {
|
||||
// Send sync event for immediate synchronization
|
||||
crate::services::events::send_sync_event(
|
||||
crate::services::events::SyncEvent::InboundChanged(server_id)
|
||||
crate::services::events::SyncEvent::InboundChanged(server_id),
|
||||
);
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
},
|
||||
}
|
||||
Ok(false) => Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
@@ -454,42 +504,43 @@ pub async fn add_user_to_inbound(
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
use crate::database::entities::inbound_users::CreateInboundUserDto;
|
||||
use crate::database::entities::user::CreateUserDto;
|
||||
|
||||
|
||||
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||
let user_repo = UserRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Get server and inbound to validate they exist
|
||||
let _server = match server_repo.find_by_id(server_id).await {
|
||||
Ok(Some(server)) => server,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
let inbound = match inbound_repo.find_by_id(inbound_id).await {
|
||||
Ok(Some(inbound)) => inbound,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Verify inbound belongs to server
|
||||
if inbound.server_id != server_id {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
// Extract user data
|
||||
|
||||
let user_name = user_data["name"].as_str()
|
||||
|
||||
let user_name = user_data["name"]
|
||||
.as_str()
|
||||
.or_else(|| user_data["username"].as_str())
|
||||
.or_else(|| user_data["email"].as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
format!("user_{}", Uuid::new_v4().to_string()[..8].to_string())
|
||||
});
|
||||
|
||||
.unwrap_or_else(|| format!("user_{}", Uuid::new_v4().to_string()[..8].to_string()));
|
||||
|
||||
let level = user_data["level"].as_u64().unwrap_or(0) as i32;
|
||||
let user_id = user_data["user_id"].as_str().and_then(|s| Uuid::parse_str(s).ok());
|
||||
|
||||
let user_id = user_data["user_id"]
|
||||
.as_str()
|
||||
.and_then(|s| Uuid::parse_str(s).ok());
|
||||
|
||||
// Get or create user
|
||||
let user = if let Some(uid) = user_id {
|
||||
// Use existing user
|
||||
@@ -506,7 +557,7 @@ pub async fn add_user_to_inbound(
|
||||
telegram_id: user_data["telegram_id"].as_i64(),
|
||||
is_telegram_admin: false,
|
||||
};
|
||||
|
||||
|
||||
match user_repo.create(create_user_dto).await {
|
||||
Ok(user) => user,
|
||||
Err(e) => {
|
||||
@@ -515,36 +566,43 @@ pub async fn add_user_to_inbound(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create inbound user repository
|
||||
let inbound_users_repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Check if user already has access to this inbound
|
||||
if inbound_users_repo.user_has_access_to_inbound(user.id, inbound_id).await.unwrap_or(false) {
|
||||
if inbound_users_repo
|
||||
.user_has_access_to_inbound(user.id, inbound_id)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
tracing::warn!("User '{}' already has access to inbound", user.name);
|
||||
return Err(StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
|
||||
// Create inbound access for user
|
||||
let inbound_user_dto = CreateInboundUserDto {
|
||||
user_id: user.id,
|
||||
server_inbound_id: inbound_id,
|
||||
level: Some(level),
|
||||
};
|
||||
|
||||
|
||||
// Grant access in database
|
||||
match inbound_users_repo.create(inbound_user_dto).await {
|
||||
Ok(created_access) => {
|
||||
tracing::info!("Granted user '{}' access to inbound (xray_id={})",
|
||||
user.name, created_access.xray_user_id);
|
||||
|
||||
tracing::info!(
|
||||
"Granted user '{}' access to inbound (xray_id={})",
|
||||
user.name,
|
||||
created_access.xray_user_id
|
||||
);
|
||||
|
||||
// Send sync event for immediate synchronization
|
||||
crate::services::events::send_sync_event(
|
||||
crate::services::events::SyncEvent::UserAccessChanged(server_id)
|
||||
crate::services::events::SyncEvent::UserAccessChanged(server_id),
|
||||
);
|
||||
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to grant user '{}' access: {}", user.name, e);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
@@ -559,25 +617,25 @@ pub async fn remove_user_from_inbound(
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
||||
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Get server and inbound
|
||||
let server = match server_repo.find_by_id(server_id).await {
|
||||
Ok(Some(server)) => server,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
let inbound = match inbound_repo.find_by_id(inbound_id).await {
|
||||
Ok(Some(inbound)) => inbound,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Verify inbound belongs to server
|
||||
if inbound.server_id != server_id {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
// Get inbound tag
|
||||
let template_repo = InboundTemplateRepository::new(app_state.db.connection().clone());
|
||||
let template = match template_repo.find_by_id(inbound.template_id).await {
|
||||
@@ -585,18 +643,22 @@ pub async fn remove_user_from_inbound(
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
let inbound_tag = &inbound.tag;
|
||||
|
||||
|
||||
// Remove user from xray server
|
||||
match app_state.xray_service.remove_user(server_id, &server.get_grpc_endpoint(), &inbound_tag, &email).await {
|
||||
match app_state
|
||||
.xray_service
|
||||
.remove_user(server_id, &server.get_grpc_endpoint(), &inbound_tag, &email)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!("Removed user '{}' from inbound", email);
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove user '{}' from inbound: {}", email, e);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{StatusCode, HeaderMap, HeaderValue},
|
||||
http::{HeaderMap, HeaderValue, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
database::repository::{UserRepository, InboundUsersRepository},
|
||||
database::repository::{InboundUsersRepository, UserRepository},
|
||||
services::uri_generator::UriGeneratorService,
|
||||
web::AppState,
|
||||
};
|
||||
@@ -21,23 +21,26 @@ pub async fn get_user_subscription(
|
||||
) -> Result<Response, StatusCode> {
|
||||
let user_repo = UserRepository::new(state.db.connection());
|
||||
let inbound_users_repo = InboundUsersRepository::new(state.db.connection().clone());
|
||||
|
||||
|
||||
// Check if user exists
|
||||
let user = match user_repo.get_by_id(user_id).await {
|
||||
Ok(Some(user)) => user,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Get all client config data for the user (this gets all active inbound accesses)
|
||||
let all_configs = match inbound_users_repo.get_all_client_configs_for_user(user_id).await {
|
||||
let all_configs = match inbound_users_repo
|
||||
.get_all_client_configs_for_user(user_id)
|
||||
.await
|
||||
{
|
||||
Ok(configs) => configs,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get client configs for user {}: {}", user_id, e);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if all_configs.is_empty() {
|
||||
let response_text = "# No configurations available\n".to_string();
|
||||
let response_base64 = general_purpose::STANDARD.encode(response_text);
|
||||
@@ -45,29 +48,38 @@ pub async fn get_user_subscription(
|
||||
StatusCode::OK,
|
||||
[("content-type", "text/plain; charset=utf-8")],
|
||||
response_base64,
|
||||
).into_response());
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
|
||||
let mut config_lines = Vec::new();
|
||||
|
||||
|
||||
// Generate connection strings for each config using existing UriGeneratorService
|
||||
let uri_generator = UriGeneratorService::new();
|
||||
|
||||
|
||||
for config_data in all_configs {
|
||||
match uri_generator.generate_client_config(user_id, &config_data) {
|
||||
Ok(client_config) => {
|
||||
config_lines.push(client_config.uri);
|
||||
tracing::debug!("Generated {} config for user {}: {}",
|
||||
config_data.protocol.to_uppercase(), user.name, config_data.template_name);
|
||||
tracing::debug!(
|
||||
"Generated {} config for user {}: {}",
|
||||
config_data.protocol.to_uppercase(),
|
||||
user.name,
|
||||
config_data.template_name
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to generate connection string for user {} template {}: {}",
|
||||
user.name, config_data.template_name, e);
|
||||
tracing::warn!(
|
||||
"Failed to generate connection string for user {} template {}: {}",
|
||||
user.name,
|
||||
config_data.template_name,
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if config_lines.is_empty() {
|
||||
let response_text = "# No valid configurations available\n".to_string();
|
||||
let response_base64 = general_purpose::STANDARD.encode(response_text);
|
||||
@@ -75,35 +87,56 @@ pub async fn get_user_subscription(
|
||||
StatusCode::OK,
|
||||
[("content-type", "text/plain; charset=utf-8")],
|
||||
response_base64,
|
||||
).into_response());
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
|
||||
// Join all URIs with newlines (like Django implementation)
|
||||
let response_text = config_lines.join("\n") + "\n";
|
||||
|
||||
|
||||
// Encode the entire response in base64 (like Django implementation)
|
||||
let response_base64 = general_purpose::STANDARD.encode(response_text);
|
||||
|
||||
|
||||
// Build response with subscription headers (like Django)
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
|
||||
// Add headers required by VPN clients
|
||||
headers.insert("content-type", HeaderValue::from_static("text/plain; charset=utf-8"));
|
||||
headers.insert("content-disposition", HeaderValue::from_str(&format!("attachment; filename=\"{}\"", user.name)).unwrap());
|
||||
headers.insert(
|
||||
"content-type",
|
||||
HeaderValue::from_static("text/plain; charset=utf-8"),
|
||||
);
|
||||
headers.insert(
|
||||
"content-disposition",
|
||||
HeaderValue::from_str(&format!("attachment; filename=\"{}\"", user.name)).unwrap(),
|
||||
);
|
||||
headers.insert("cache-control", HeaderValue::from_static("no-cache"));
|
||||
|
||||
|
||||
// Profile information
|
||||
let profile_title = general_purpose::STANDARD.encode("OutFleet VPN");
|
||||
headers.insert("profile-title", HeaderValue::from_str(&format!("base64:{}", profile_title)).unwrap());
|
||||
headers.insert(
|
||||
"profile-title",
|
||||
HeaderValue::from_str(&format!("base64:{}", profile_title)).unwrap(),
|
||||
);
|
||||
headers.insert("profile-update-interval", HeaderValue::from_static("24"));
|
||||
headers.insert("profile-web-page-url", HeaderValue::from_str(&format!("{}/u/{}", state.config.web.base_url, user_id)).unwrap());
|
||||
headers.insert("support-url", HeaderValue::from_str(&format!("{}/admin/", state.config.web.base_url)).unwrap());
|
||||
|
||||
headers.insert(
|
||||
"profile-web-page-url",
|
||||
HeaderValue::from_str(&format!("{}/u/{}", state.config.web.base_url, user_id)).unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
"support-url",
|
||||
HeaderValue::from_str(&format!("{}/admin/", state.config.web.base_url)).unwrap(),
|
||||
);
|
||||
|
||||
// Subscription info (unlimited service)
|
||||
let expire_timestamp = chrono::Utc::now().timestamp() + (365 * 24 * 60 * 60); // 1 year from now
|
||||
headers.insert("subscription-userinfo",
|
||||
HeaderValue::from_str(&format!("upload=0; download=0; total=1099511627776; expire={}", expire_timestamp)).unwrap());
|
||||
|
||||
headers.insert(
|
||||
"subscription-userinfo",
|
||||
HeaderValue::from_str(&format!(
|
||||
"upload=0; download=0; total=1099511627776; expire={}",
|
||||
expire_timestamp
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
Ok((StatusCode::OK, headers, response_base64).into_response())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
};
|
||||
use axum::{extract::State, http::StatusCode, response::Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -45,42 +41,58 @@ pub async fn get_tasks_status(
|
||||
// Get task status from the scheduler
|
||||
// For now, we'll return a mock response since we need to expose the scheduler
|
||||
// In a real implementation, you'd store a reference to the TaskScheduler in AppState
|
||||
|
||||
|
||||
let mut tasks = HashMap::new();
|
||||
let mut running_count = 0;
|
||||
let mut success_count = 0;
|
||||
let mut error_count = 0;
|
||||
let mut idle_count = 0;
|
||||
|
||||
|
||||
// Mock data for demonstration - in real implementation, get from TaskScheduler
|
||||
let xray_sync_task = TaskStatusResponse {
|
||||
name: "Xray Synchronization".to_string(),
|
||||
description: "Synchronizes database state with xray servers".to_string(),
|
||||
schedule: "0 */5 * * * * (every 5 minutes)".to_string(),
|
||||
status: "Success".to_string(),
|
||||
last_run: Some(chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string()),
|
||||
next_run: Some((chrono::Utc::now() + chrono::Duration::minutes(5)).format("%Y-%m-%d %H:%M:%S UTC").to_string()),
|
||||
last_run: Some(
|
||||
chrono::Utc::now()
|
||||
.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
.to_string(),
|
||||
),
|
||||
next_run: Some(
|
||||
(chrono::Utc::now() + chrono::Duration::minutes(5))
|
||||
.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
.to_string(),
|
||||
),
|
||||
total_runs: 120,
|
||||
success_count: 118,
|
||||
error_count: 2,
|
||||
last_error: None,
|
||||
last_duration_ms: Some(1234),
|
||||
};
|
||||
|
||||
|
||||
let cert_renewal_task = TaskStatusResponse {
|
||||
name: "Certificate Renewal".to_string(),
|
||||
description: "Renews Let's Encrypt certificates that expire within 15 days".to_string(),
|
||||
schedule: "0 0 2 * * * (daily at 2 AM)".to_string(),
|
||||
status: "Idle".to_string(),
|
||||
last_run: Some((chrono::Utc::now() - chrono::Duration::hours(8)).format("%Y-%m-%d %H:%M:%S UTC").to_string()),
|
||||
next_run: Some((chrono::Utc::now() + chrono::Duration::hours(16)).format("%Y-%m-%d %H:%M:%S UTC").to_string()),
|
||||
last_run: Some(
|
||||
(chrono::Utc::now() - chrono::Duration::hours(8))
|
||||
.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
.to_string(),
|
||||
),
|
||||
next_run: Some(
|
||||
(chrono::Utc::now() + chrono::Duration::hours(16))
|
||||
.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
.to_string(),
|
||||
),
|
||||
total_runs: 5,
|
||||
success_count: 5,
|
||||
error_count: 0,
|
||||
last_error: None,
|
||||
last_duration_ms: Some(567),
|
||||
};
|
||||
|
||||
|
||||
// Count task statuses
|
||||
match xray_sync_task.status.as_str() {
|
||||
"Running" => running_count += 1,
|
||||
@@ -89,7 +101,7 @@ pub async fn get_tasks_status(
|
||||
"Idle" => idle_count += 1,
|
||||
_ => idle_count += 1,
|
||||
}
|
||||
|
||||
|
||||
match cert_renewal_task.status.as_str() {
|
||||
"Running" => running_count += 1,
|
||||
"Success" => success_count += 1,
|
||||
@@ -97,10 +109,10 @@ pub async fn get_tasks_status(
|
||||
"Idle" => idle_count += 1,
|
||||
_ => idle_count += 1,
|
||||
}
|
||||
|
||||
|
||||
tasks.insert("xray_sync".to_string(), xray_sync_task);
|
||||
tasks.insert("cert_renewal".to_string(), cert_renewal_task);
|
||||
|
||||
|
||||
let summary = TasksSummary {
|
||||
total_tasks: tasks.len(),
|
||||
running_tasks: running_count,
|
||||
@@ -108,9 +120,9 @@ pub async fn get_tasks_status(
|
||||
failed_tasks: error_count,
|
||||
idle_tasks: idle_count,
|
||||
};
|
||||
|
||||
|
||||
let response = TasksStatusResponse { tasks, summary };
|
||||
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
@@ -122,14 +134,10 @@ pub async fn trigger_task(
|
||||
// In a real implementation, you'd trigger the actual task
|
||||
// For now, return a success response
|
||||
match task_id.as_str() {
|
||||
"xray_sync" | "cert_renewal" => {
|
||||
Ok(Json(serde_json::json!({
|
||||
"success": true,
|
||||
"message": format!("Task '{}' has been triggered", task_id)
|
||||
})))
|
||||
}
|
||||
_ => {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
}
|
||||
"xray_sync" | "cert_renewal" => Ok(Json(serde_json::json!({
|
||||
"success": true,
|
||||
"message": format!("Task '{}' has been triggered", task_id)
|
||||
}))),
|
||||
_ => Err(StatusCode::NOT_FOUND),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use axum::{
|
||||
extract::{State, Path, Json},
|
||||
extract::{Json, Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::telegram_config::{
|
||||
CreateTelegramConfigDto, UpdateTelegramConfigDto,
|
||||
};
|
||||
use crate::database::repository::{TelegramConfigRepository, UserRepository};
|
||||
use crate::web::AppState;
|
||||
use crate::database::repository::{UserRepository, TelegramConfigRepository};
|
||||
use crate::database::entities::telegram_config::{CreateTelegramConfigDto, UpdateTelegramConfigDto};
|
||||
|
||||
/// Response for Telegram config
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -27,11 +29,9 @@ pub struct BotInfo {
|
||||
}
|
||||
|
||||
/// Get current Telegram configuration
|
||||
pub async fn get_telegram_config(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
pub async fn get_telegram_config(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let repo = TelegramConfigRepository::new(state.db.connection());
|
||||
|
||||
|
||||
match repo.get_latest().await {
|
||||
Ok(Some(config)) => {
|
||||
let mut response = TelegramConfigResponse {
|
||||
@@ -51,9 +51,7 @@ pub async fn get_telegram_config(
|
||||
|
||||
Json(response).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get telegram config: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
@@ -67,7 +65,7 @@ pub async fn create_telegram_config(
|
||||
Json(dto): Json<CreateTelegramConfigDto>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = TelegramConfigRepository::new(state.db.connection());
|
||||
|
||||
|
||||
match repo.create(dto).await {
|
||||
Ok(config) => {
|
||||
// Initialize telegram service with new config if active
|
||||
@@ -76,7 +74,7 @@ pub async fn create_telegram_config(
|
||||
let _ = telegram_service.update_config(config.id).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(StatusCode::CREATED, Json(config)).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -93,19 +91,17 @@ pub async fn update_telegram_config(
|
||||
Json(dto): Json<UpdateTelegramConfigDto>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = TelegramConfigRepository::new(state.db.connection());
|
||||
|
||||
|
||||
match repo.update(id, dto).await {
|
||||
Ok(Some(config)) => {
|
||||
// Update telegram service
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
let _ = telegram_service.update_config(config.id).await;
|
||||
}
|
||||
|
||||
|
||||
Json(config).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to update telegram config: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
@@ -119,7 +115,7 @@ pub async fn delete_telegram_config(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = TelegramConfigRepository::new(state.db.connection());
|
||||
|
||||
|
||||
// Stop bot if this config is active
|
||||
if let Ok(Some(config)) = repo.find_by_id(id).await {
|
||||
if config.is_active {
|
||||
@@ -128,7 +124,7 @@ pub async fn delete_telegram_config(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
match repo.delete(id).await {
|
||||
Ok(true) => StatusCode::NO_CONTENT.into_response(),
|
||||
Ok(false) => StatusCode::NOT_FOUND.into_response(),
|
||||
@@ -149,7 +145,7 @@ pub struct BotStatusResponse {
|
||||
async fn get_bot_status(state: &AppState) -> Result<BotStatusResponse, String> {
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
let status = telegram_service.get_status().await;
|
||||
|
||||
|
||||
let bot_info = if status.is_running {
|
||||
// In production, you would get this from the bot API
|
||||
Some(BotInfo {
|
||||
@@ -159,7 +155,7 @@ async fn get_bot_status(state: &AppState) -> Result<BotStatusResponse, String> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
|
||||
Ok(BotStatusResponse {
|
||||
is_running: status.is_running,
|
||||
bot_info,
|
||||
@@ -172,9 +168,7 @@ async fn get_bot_status(state: &AppState) -> Result<BotStatusResponse, String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_telegram_status(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
pub async fn get_telegram_status(State(state): State<AppState>) -> impl IntoResponse {
|
||||
match get_bot_status(&state).await {
|
||||
Ok(status) => Json(status).into_response(),
|
||||
Err(e) => {
|
||||
@@ -192,11 +186,9 @@ pub struct TelegramAdmin {
|
||||
pub telegram_id: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn get_telegram_admins(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
pub async fn get_telegram_admins(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let repo = UserRepository::new(state.db.connection());
|
||||
|
||||
|
||||
match repo.get_telegram_admins().await {
|
||||
Ok(admins) => {
|
||||
let response: Vec<TelegramAdmin> = admins
|
||||
@@ -207,7 +199,7 @@ pub async fn get_telegram_admins(
|
||||
telegram_id: u.telegram_id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
Json(response).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -223,24 +215,24 @@ pub async fn add_telegram_admin(
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = UserRepository::new(state.db.connection());
|
||||
|
||||
|
||||
match repo.set_telegram_admin(user_id, true).await {
|
||||
Ok(Some(user)) => {
|
||||
// Notify via Telegram if bot is running
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
if let Some(telegram_id) = user.telegram_id {
|
||||
let _ = telegram_service.send_message(
|
||||
telegram_id,
|
||||
"✅ You have been granted admin privileges!".to_string()
|
||||
).await;
|
||||
let _ = telegram_service
|
||||
.send_message(
|
||||
telegram_id,
|
||||
"✅ You have been granted admin privileges!".to_string(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Json(user).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to add telegram admin: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
@@ -254,24 +246,24 @@ pub async fn remove_telegram_admin(
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = UserRepository::new(state.db.connection());
|
||||
|
||||
|
||||
match repo.set_telegram_admin(user_id, false).await {
|
||||
Ok(Some(user)) => {
|
||||
// Notify via Telegram if bot is running
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
if let Some(telegram_id) = user.telegram_id {
|
||||
let _ = telegram_service.send_message(
|
||||
telegram_id,
|
||||
"❌ Your admin privileges have been revoked.".to_string()
|
||||
).await;
|
||||
let _ = telegram_service
|
||||
.send_message(
|
||||
telegram_id,
|
||||
"❌ Your admin privileges have been revoked.".to_string(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Json(user).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove telegram admin: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
@@ -301,4 +293,4 @@ pub async fn send_test_message(
|
||||
} else {
|
||||
StatusCode::SERVICE_UNAVAILABLE.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use crate::{
|
||||
database::{entities::inbound_template, repository::InboundTemplateRepository},
|
||||
web::AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
@@ -5,26 +9,17 @@ use axum::{
|
||||
Json as JsonExtractor,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use crate::{
|
||||
database::{
|
||||
entities::inbound_template,
|
||||
repository::InboundTemplateRepository,
|
||||
},
|
||||
web::AppState,
|
||||
};
|
||||
|
||||
/// List all inbound templates
|
||||
pub async fn list_templates(
|
||||
State(app_state): State<AppState>,
|
||||
) -> Result<Json<Vec<inbound_template::InboundTemplateResponse>>, StatusCode> {
|
||||
let repo = InboundTemplateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_all().await {
|
||||
Ok(templates) => {
|
||||
let responses: Vec<inbound_template::InboundTemplateResponse> = templates
|
||||
.into_iter()
|
||||
.map(|t| t.into())
|
||||
.collect();
|
||||
let responses: Vec<inbound_template::InboundTemplateResponse> =
|
||||
templates.into_iter().map(|t| t.into()).collect();
|
||||
Ok(Json(responses))
|
||||
}
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -37,7 +32,7 @@ pub async fn get_template(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<inbound_template::InboundTemplateResponse>, StatusCode> {
|
||||
let repo = InboundTemplateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.find_by_id(id).await {
|
||||
Ok(Some(template)) => Ok(Json(template.into())),
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -52,7 +47,7 @@ pub async fn create_template(
|
||||
) -> Result<Json<inbound_template::InboundTemplateResponse>, StatusCode> {
|
||||
tracing::info!("Creating template: {:?}", template_data);
|
||||
let repo = InboundTemplateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.create(template_data).await {
|
||||
Ok(template) => Ok(Json(template.into())),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -66,7 +61,7 @@ pub async fn update_template(
|
||||
JsonExtractor(template_data): JsonExtractor<inbound_template::UpdateInboundTemplateDto>,
|
||||
) -> Result<Json<inbound_template::InboundTemplateResponse>, StatusCode> {
|
||||
let repo = InboundTemplateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.update(id, template_data).await {
|
||||
Ok(template) => Ok(Json(template.into())),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@@ -79,10 +74,10 @@ pub async fn delete_template(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let repo = InboundTemplateRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
match repo.delete(id).await {
|
||||
Ok(true) => Ok(StatusCode::NO_CONTENT),
|
||||
Ok(false) => Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json,
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
database::entities::user_request::{CreateUserRequestDto, UpdateUserRequestDto, RequestStatus},
|
||||
database::entities::user_request::{CreateUserRequestDto, RequestStatus, UpdateUserRequestDto},
|
||||
database::repository::UserRequestRepository,
|
||||
services::telegram::localization::{Language, LocalizationService},
|
||||
web::AppState,
|
||||
services::telegram::localization::{LocalizationService, Language},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -23,8 +23,12 @@ pub struct RequestsQuery {
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
fn default_page() -> u64 { 1 }
|
||||
fn default_per_page() -> u64 { 20 }
|
||||
fn default_page() -> u64 {
|
||||
1
|
||||
}
|
||||
fn default_per_page() -> u64 {
|
||||
20
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RequestsResponse {
|
||||
@@ -85,11 +89,20 @@ pub async fn get_requests(
|
||||
let (items, total) = if let Some(status) = query.status {
|
||||
// Filter by status
|
||||
match status.as_str() {
|
||||
"pending" => request_repo.find_pending(query.page, query.per_page).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
|
||||
_ => request_repo.find_all(query.page, query.per_page).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
|
||||
"pending" => request_repo
|
||||
.find_pending(query.page, query.per_page)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
|
||||
_ => request_repo
|
||||
.find_all(query.page, query.per_page)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
|
||||
}
|
||||
} else {
|
||||
request_repo.find_all(query.page, query.per_page).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
request_repo
|
||||
.find_all(query.page, query.per_page)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
};
|
||||
|
||||
let items: Vec<UserRequestResponse> = items.into_iter().map(Into::into).collect();
|
||||
@@ -108,7 +121,7 @@ pub async fn get_request(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<UserRequestResponse>, StatusCode> {
|
||||
let request_repo = UserRequestRepository::new(state.db.connection());
|
||||
|
||||
|
||||
match request_repo.find_by_id(id).await {
|
||||
Ok(Some(request)) => Ok(Json(UserRequestResponse::from(request))),
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -129,19 +142,19 @@ pub async fn approve_request(
|
||||
) -> Result<Json<UserRequestResponse>, StatusCode> {
|
||||
let request_repo = UserRequestRepository::new(state.db.connection());
|
||||
let user_repo = crate::database::repository::UserRepository::new(state.db.connection());
|
||||
|
||||
|
||||
// Get the request
|
||||
let request = match request_repo.find_by_id(id).await {
|
||||
Ok(Some(request)) => request,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Check if already processed
|
||||
if request.status != "pending" {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
// Create user account
|
||||
let username = request.telegram_username.as_deref().unwrap_or("Unknown");
|
||||
let user_dto = crate::database::entities::user::CreateUserDto {
|
||||
@@ -150,7 +163,7 @@ pub async fn approve_request(
|
||||
telegram_id: Some(request.telegram_id),
|
||||
is_telegram_admin: false,
|
||||
};
|
||||
|
||||
|
||||
match user_repo.create(user_dto).await {
|
||||
Ok(new_user) => {
|
||||
// Get the first admin user ID (for web approvals we don't have a specific admin)
|
||||
@@ -162,48 +175,66 @@ pub async fn approve_request(
|
||||
Uuid::new_v4()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Approve the request
|
||||
let approved = match request_repo.approve(id, dto.response_message, admin_id).await {
|
||||
let approved = match request_repo
|
||||
.approve(id, dto.response_message, admin_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(approved)) => approved,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Send main menu to the user instead of just notification
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
let user_lang = Language::from_telegram_code(Some(&request.get_language()));
|
||||
let l10n = LocalizationService::new();
|
||||
|
||||
|
||||
// Check if user is admin (new users are not admins by default)
|
||||
let is_admin = false;
|
||||
|
||||
|
||||
// Build main menu keyboard
|
||||
let keyboard = if is_admin {
|
||||
vec![
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(l10n.get(user_lang.clone(), "my_configs"), "my_configs")],
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(l10n.get(user_lang.clone(), "support"), "support")],
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(l10n.get(user_lang.clone(), "user_requests"), "admin_requests")],
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(
|
||||
l10n.get(user_lang.clone(), "my_configs"),
|
||||
"my_configs",
|
||||
)],
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(
|
||||
l10n.get(user_lang.clone(), "support"),
|
||||
"support",
|
||||
)],
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(
|
||||
l10n.get(user_lang.clone(), "user_requests"),
|
||||
"admin_requests",
|
||||
)],
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(l10n.get(user_lang.clone(), "my_configs"), "my_configs")],
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(l10n.get(user_lang.clone(), "support"), "support")],
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(
|
||||
l10n.get(user_lang.clone(), "my_configs"),
|
||||
"my_configs",
|
||||
)],
|
||||
vec![teloxide::types::InlineKeyboardButton::callback(
|
||||
l10n.get(user_lang.clone(), "support"),
|
||||
"support",
|
||||
)],
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
let keyboard_markup = teloxide::types::InlineKeyboardMarkup::new(keyboard);
|
||||
let message = l10n.format(user_lang, "welcome_back", &[("name", &new_user.name)]);
|
||||
|
||||
|
||||
// Send message with keyboard
|
||||
let _ = telegram_service.send_message_with_keyboard(request.telegram_id, message, keyboard_markup).await;
|
||||
let _ = telegram_service
|
||||
.send_message_with_keyboard(request.telegram_id, message, keyboard_markup)
|
||||
.await;
|
||||
}
|
||||
|
||||
|
||||
Ok(Json(UserRequestResponse::from(approved)))
|
||||
}
|
||||
Err(_) => {
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
Err(_) => Err(StatusCode::BAD_REQUEST),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,19 +250,19 @@ pub async fn decline_request(
|
||||
Json(dto): Json<DeclineRequestDto>,
|
||||
) -> Result<Json<UserRequestResponse>, StatusCode> {
|
||||
let request_repo = UserRequestRepository::new(state.db.connection());
|
||||
|
||||
|
||||
// Get the request
|
||||
let request = match request_repo.find_by_id(id).await {
|
||||
Ok(Some(request)) => request,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Check if already processed
|
||||
if request.status != "pending" {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
// Get the first admin user ID (for web declines we don't have a specific admin)
|
||||
let user_repo = crate::database::repository::UserRepository::new(state.db.connection());
|
||||
let admin_id = match user_repo.get_first_admin().await {
|
||||
@@ -241,24 +272,29 @@ pub async fn decline_request(
|
||||
Uuid::new_v4()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Decline the request
|
||||
let declined = match request_repo.decline(id, dto.response_message, admin_id).await {
|
||||
let declined = match request_repo
|
||||
.decline(id, dto.response_message, admin_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(declined)) => declined,
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
|
||||
|
||||
// Send Telegram notification to user
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
let user_lang = Language::from_telegram_code(Some(&request.get_language()));
|
||||
let l10n = LocalizationService::new();
|
||||
let user_message = l10n.get(user_lang, "request_declined_notification");
|
||||
|
||||
|
||||
// Send notification (ignore errors - don't fail the request)
|
||||
let _ = telegram_service.send_message(request.telegram_id, user_message).await;
|
||||
let _ = telegram_service
|
||||
.send_message(request.telegram_id, user_message)
|
||||
.await;
|
||||
}
|
||||
|
||||
|
||||
Ok(Json(UserRequestResponse::from(declined)))
|
||||
}
|
||||
|
||||
@@ -268,10 +304,12 @@ pub async fn delete_request(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
let request_repo = UserRequestRepository::new(state.db.connection());
|
||||
|
||||
|
||||
match request_repo.delete(id).await {
|
||||
Ok(true) => Ok(Json(serde_json::json!({ "message": "User request deleted" }))),
|
||||
Ok(true) => Ok(Json(
|
||||
serde_json::json!({ "message": "User request deleted" }),
|
||||
)),
|
||||
Ok(false) => Err(StatusCode::NOT_FOUND),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::user::{CreateUserDto, UpdateUserDto, Model as UserModel};
|
||||
use crate::database::entities::user::{CreateUserDto, Model as UserModel, UpdateUserDto};
|
||||
use crate::database::repository::UserRepository;
|
||||
use crate::web::AppState;
|
||||
|
||||
@@ -45,8 +45,12 @@ pub struct UserResponse {
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
fn default_page() -> u64 { 1 }
|
||||
fn default_per_page() -> u64 { 20 }
|
||||
fn default_page() -> u64 {
|
||||
1
|
||||
}
|
||||
fn default_per_page() -> u64 {
|
||||
20
|
||||
}
|
||||
|
||||
impl From<UserModel> for UserResponse {
|
||||
fn from(user: UserModel) -> Self {
|
||||
@@ -67,12 +71,14 @@ pub async fn get_users(
|
||||
Query(query): Query<PaginationQuery>,
|
||||
) -> Result<Json<UsersResponse>, StatusCode> {
|
||||
let repo = UserRepository::new(app_state.db.connection().clone());
|
||||
|
||||
let users = repo.get_all(query.page, query.per_page)
|
||||
|
||||
let users = repo
|
||||
.get_all(query.page, query.per_page)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let total = repo.count()
|
||||
|
||||
let total = repo
|
||||
.count()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
@@ -92,7 +98,7 @@ pub async fn search_users(
|
||||
Query(query): Query<SearchQuery>,
|
||||
) -> Result<Json<Vec<UserResponse>>, StatusCode> {
|
||||
let repo = UserRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
let users = if let Some(search_query) = query.q {
|
||||
// Search by name, telegram_id, or UUID
|
||||
repo.search(&search_query)
|
||||
@@ -113,8 +119,9 @@ pub async fn get_user(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<UserResponse>, StatusCode> {
|
||||
let repo = UserRepository::new(app_state.db.connection().clone());
|
||||
|
||||
let user = repo.get_by_id(id)
|
||||
|
||||
let user = repo
|
||||
.get_by_id(id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
@@ -130,19 +137,21 @@ pub async fn create_user(
|
||||
JsonExtractor(dto): JsonExtractor<CreateUserDto>,
|
||||
) -> Result<Json<UserResponse>, StatusCode> {
|
||||
let repo = UserRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Check if telegram ID is already in use
|
||||
if let Some(telegram_id) = dto.telegram_id {
|
||||
let exists = repo.telegram_id_exists(telegram_id)
|
||||
let exists = repo
|
||||
.telegram_id_exists(telegram_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
if exists {
|
||||
return Err(StatusCode::CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
let user = repo.create(dto)
|
||||
let user = repo
|
||||
.create(dto)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
@@ -156,18 +165,22 @@ pub async fn update_user(
|
||||
JsonExtractor(dto): JsonExtractor<UpdateUserDto>,
|
||||
) -> Result<Json<UserResponse>, StatusCode> {
|
||||
let repo = UserRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
// Check if telegram ID is already in use by another user
|
||||
if let Some(telegram_id) = dto.telegram_id {
|
||||
if let Some(existing_user) = repo.get_by_telegram_id(telegram_id).await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
||||
if let Some(existing_user) = repo
|
||||
.get_by_telegram_id(telegram_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
{
|
||||
if existing_user.id != id {
|
||||
return Err(StatusCode::CONFLICT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let user = repo.update(id, dto)
|
||||
let user = repo
|
||||
.update(id, dto)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
@@ -183,8 +196,9 @@ pub async fn delete_user(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<Value>, StatusCode> {
|
||||
let repo = UserRepository::new(app_state.db.connection().clone());
|
||||
|
||||
let deleted = repo.delete(id)
|
||||
|
||||
let deleted = repo
|
||||
.delete(id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
@@ -203,19 +217,19 @@ pub async fn get_user_access(
|
||||
) -> Result<Json<Vec<serde_json::Value>>, StatusCode> {
|
||||
use crate::database::repository::InboundUsersRepository;
|
||||
use crate::services::UriGeneratorService;
|
||||
|
||||
|
||||
let inbound_users_repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
|
||||
|
||||
let access_list = inbound_users_repo
|
||||
.find_by_user_id(user_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
let mut response: Vec<serde_json::Value> = Vec::new();
|
||||
|
||||
|
||||
if query.include_uris {
|
||||
let uri_service = UriGeneratorService::new();
|
||||
|
||||
|
||||
for access in access_list {
|
||||
let mut access_json = serde_json::json!({
|
||||
"id": access.id,
|
||||
@@ -225,37 +239,43 @@ pub async fn get_user_access(
|
||||
"level": access.level,
|
||||
"is_active": access.is_active,
|
||||
});
|
||||
|
||||
|
||||
// Try to get client config and generate URI
|
||||
if access.is_active {
|
||||
if let Ok(Some(config_data)) = inbound_users_repo
|
||||
.get_client_config_data(user_id, access.server_inbound_id)
|
||||
.await {
|
||||
|
||||
if let Ok(client_config) = uri_service.generate_client_config(user_id, &config_data) {
|
||||
.await
|
||||
{
|
||||
if let Ok(client_config) =
|
||||
uri_service.generate_client_config(user_id, &config_data)
|
||||
{
|
||||
access_json["uri"] = serde_json::Value::String(client_config.uri);
|
||||
access_json["protocol"] = serde_json::Value::String(client_config.protocol);
|
||||
access_json["server_name"] = serde_json::Value::String(client_config.server_name);
|
||||
access_json["inbound_tag"] = serde_json::Value::String(client_config.inbound_tag);
|
||||
access_json["server_name"] =
|
||||
serde_json::Value::String(client_config.server_name);
|
||||
access_json["inbound_tag"] =
|
||||
serde_json::Value::String(client_config.inbound_tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
response.push(access_json);
|
||||
}
|
||||
} else {
|
||||
response = access_list
|
||||
.into_iter()
|
||||
.map(|access| serde_json::json!({
|
||||
"id": access.id,
|
||||
"user_id": access.user_id,
|
||||
"server_inbound_id": access.server_inbound_id,
|
||||
"xray_user_id": access.xray_user_id,
|
||||
"level": access.level,
|
||||
"is_active": access.is_active,
|
||||
}))
|
||||
.map(|access| {
|
||||
serde_json::json!({
|
||||
"id": access.id,
|
||||
"user_id": access.user_id,
|
||||
"server_inbound_id": access.server_inbound_id,
|
||||
"xray_user_id": access.xray_user_id,
|
||||
"level": access.level,
|
||||
"is_active": access.is_active,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
serve,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Json, routing::get, serve, Router};
|
||||
use serde_json::{json, Value};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
@@ -13,10 +7,10 @@ use tower_http::cors::CorsLayer;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing::info;
|
||||
|
||||
use std::sync::Arc;
|
||||
use crate::config::{WebConfig, AppConfig};
|
||||
use crate::config::{AppConfig, WebConfig};
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::services::{XrayService, TelegramService};
|
||||
use crate::services::{TelegramService, XrayService};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
@@ -33,9 +27,13 @@ pub struct AppState {
|
||||
}
|
||||
|
||||
/// Start the web server
|
||||
pub async fn start_server(db: DatabaseManager, config: AppConfig, telegram_service: Option<Arc<TelegramService>>) -> Result<()> {
|
||||
pub async fn start_server(
|
||||
db: DatabaseManager,
|
||||
config: AppConfig,
|
||||
telegram_service: Option<Arc<TelegramService>>,
|
||||
) -> Result<()> {
|
||||
let xray_service = XrayService::new();
|
||||
|
||||
|
||||
let app_state = AppState {
|
||||
db,
|
||||
config: config.clone(),
|
||||
@@ -70,4 +68,4 @@ async fn health_check() -> Result<Json<Value>, StatusCode> {
|
||||
"service": "xray-admin",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use axum::{
|
||||
routing::{delete, get, post, put},
|
||||
Router,
|
||||
routing::{get, post, put, delete},
|
||||
};
|
||||
|
||||
use crate::web::{AppState, handlers};
|
||||
use crate::web::{handlers, AppState};
|
||||
|
||||
pub mod servers;
|
||||
|
||||
@@ -25,22 +25,37 @@ fn user_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(handlers::get_users).post(handlers::create_user))
|
||||
.route("/search", get(handlers::search_users))
|
||||
.route("/:id", get(handlers::get_user)
|
||||
.put(handlers::update_user)
|
||||
.delete(handlers::delete_user))
|
||||
.route(
|
||||
"/:id",
|
||||
get(handlers::get_user)
|
||||
.put(handlers::update_user)
|
||||
.delete(handlers::delete_user),
|
||||
)
|
||||
.route("/:id/access", get(handlers::get_user_access))
|
||||
.route("/:user_id/configs", get(handlers::get_user_configs))
|
||||
.route("/:user_id/access/:inbound_id/config", get(handlers::get_user_inbound_config))
|
||||
.route(
|
||||
"/:user_id/access/:inbound_id/config",
|
||||
get(handlers::get_user_inbound_config),
|
||||
)
|
||||
}
|
||||
|
||||
/// DNS Provider management routes
|
||||
fn dns_provider_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(handlers::list_dns_providers).post(handlers::create_dns_provider))
|
||||
.route("/:id", get(handlers::get_dns_provider)
|
||||
.put(handlers::update_dns_provider)
|
||||
.delete(handlers::delete_dns_provider))
|
||||
.route("/cloudflare/active", get(handlers::list_active_cloudflare_providers))
|
||||
.route(
|
||||
"/",
|
||||
get(handlers::list_dns_providers).post(handlers::create_dns_provider),
|
||||
)
|
||||
.route(
|
||||
"/:id",
|
||||
get(handlers::get_dns_provider)
|
||||
.put(handlers::update_dns_provider)
|
||||
.delete(handlers::delete_dns_provider),
|
||||
)
|
||||
.route(
|
||||
"/cloudflare/active",
|
||||
get(handlers::list_active_cloudflare_providers),
|
||||
)
|
||||
}
|
||||
|
||||
/// Task management routes
|
||||
@@ -53,17 +68,22 @@ fn task_routes() -> Router<AppState> {
|
||||
/// Telegram bot management routes
|
||||
fn telegram_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/config", get(handlers::get_telegram_config)
|
||||
.post(handlers::create_telegram_config))
|
||||
.route("/config/:id",
|
||||
.route(
|
||||
"/config",
|
||||
get(handlers::get_telegram_config).post(handlers::create_telegram_config),
|
||||
)
|
||||
.route(
|
||||
"/config/:id",
|
||||
get(handlers::get_telegram_config)
|
||||
.put(handlers::update_telegram_config)
|
||||
.delete(handlers::delete_telegram_config))
|
||||
.put(handlers::update_telegram_config)
|
||||
.delete(handlers::delete_telegram_config),
|
||||
)
|
||||
.route("/status", get(handlers::get_telegram_status))
|
||||
.route("/admins", get(handlers::get_telegram_admins))
|
||||
.route("/admins/:user_id",
|
||||
post(handlers::add_telegram_admin)
|
||||
.delete(handlers::remove_telegram_admin))
|
||||
.route(
|
||||
"/admins/:user_id",
|
||||
post(handlers::add_telegram_admin).delete(handlers::remove_telegram_admin),
|
||||
)
|
||||
.route("/send", post(handlers::send_test_message))
|
||||
}
|
||||
|
||||
@@ -71,7 +91,10 @@ fn telegram_routes() -> Router<AppState> {
|
||||
fn user_request_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(handlers::get_requests))
|
||||
.route("/:id", get(handlers::get_request).delete(handlers::delete_request))
|
||||
.route(
|
||||
"/:id",
|
||||
get(handlers::get_request).delete(handlers::delete_request),
|
||||
)
|
||||
.route("/:id/approve", post(handlers::approve_request))
|
||||
.route("/:id/decline", post(handlers::decline_request))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,77 @@
|
||||
use crate::web::{handlers, AppState};
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use crate::{
|
||||
web::{AppState, handlers},
|
||||
};
|
||||
|
||||
pub fn server_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
// Server management
|
||||
.route("/", get(handlers::list_servers).post(handlers::create_server))
|
||||
.route("/:id", get(handlers::get_server).put(handlers::update_server).delete(handlers::delete_server))
|
||||
.route(
|
||||
"/",
|
||||
get(handlers::list_servers).post(handlers::create_server),
|
||||
)
|
||||
.route(
|
||||
"/:id",
|
||||
get(handlers::get_server)
|
||||
.put(handlers::update_server)
|
||||
.delete(handlers::delete_server),
|
||||
)
|
||||
.route("/:id/test", post(handlers::test_server_connection))
|
||||
.route("/:id/stats", get(handlers::get_server_stats))
|
||||
|
||||
// Server inbounds
|
||||
.route("/:server_id/inbounds", get(handlers::list_server_inbounds).post(handlers::create_server_inbound))
|
||||
.route("/:server_id/inbounds/:inbound_id", get(handlers::get_server_inbound).put(handlers::update_server_inbound).delete(handlers::delete_server_inbound))
|
||||
|
||||
.route(
|
||||
"/:server_id/inbounds",
|
||||
get(handlers::list_server_inbounds).post(handlers::create_server_inbound),
|
||||
)
|
||||
.route(
|
||||
"/:server_id/inbounds/:inbound_id",
|
||||
get(handlers::get_server_inbound)
|
||||
.put(handlers::update_server_inbound)
|
||||
.delete(handlers::delete_server_inbound),
|
||||
)
|
||||
// User management for inbounds
|
||||
.route("/:server_id/inbounds/:inbound_id/users", post(handlers::add_user_to_inbound))
|
||||
.route("/:server_id/inbounds/:inbound_id/users/:email", axum::routing::delete(handlers::remove_user_from_inbound))
|
||||
|
||||
.route(
|
||||
"/:server_id/inbounds/:inbound_id/users",
|
||||
post(handlers::add_user_to_inbound),
|
||||
)
|
||||
.route(
|
||||
"/:server_id/inbounds/:inbound_id/users/:email",
|
||||
axum::routing::delete(handlers::remove_user_from_inbound),
|
||||
)
|
||||
// Client configurations for inbounds
|
||||
.route("/:server_id/inbounds/:inbound_id/configs", get(handlers::get_inbound_configs))
|
||||
.route(
|
||||
"/:server_id/inbounds/:inbound_id/configs",
|
||||
get(handlers::get_inbound_configs),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn certificate_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(handlers::list_certificates).post(handlers::create_certificate))
|
||||
.route("/:id", get(handlers::get_certificate).put(handlers::update_certificate).delete(handlers::delete_certificate))
|
||||
.route(
|
||||
"/",
|
||||
get(handlers::list_certificates).post(handlers::create_certificate),
|
||||
)
|
||||
.route(
|
||||
"/:id",
|
||||
get(handlers::get_certificate)
|
||||
.put(handlers::update_certificate)
|
||||
.delete(handlers::delete_certificate),
|
||||
)
|
||||
.route("/:id/details", get(handlers::get_certificate_details))
|
||||
.route("/expiring", get(handlers::get_expiring_certificates))
|
||||
}
|
||||
|
||||
pub fn template_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(handlers::list_templates).post(handlers::create_template))
|
||||
.route("/:id", get(handlers::get_template).put(handlers::update_template).delete(handlers::delete_template))
|
||||
}
|
||||
.route(
|
||||
"/",
|
||||
get(handlers::list_templates).post(handlers::create_template),
|
||||
)
|
||||
.route(
|
||||
"/:id",
|
||||
get(handlers::get_template)
|
||||
.put(handlers::update_template)
|
||||
.delete(handlers::delete_template),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user