mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-24 17:29:08 +00:00
Useradd works
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -4083,6 +4083,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"log",
|
"log",
|
||||||
"prost",
|
"prost",
|
||||||
|
"rand",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
# Web server
|
# Web server
|
||||||
axum = { version = "0.7", features = ["macros", "json"] }
|
axum = { version = "0.7", features = ["macros", "json"] }
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ pub struct Model {
|
|||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
||||||
|
/// Reference to the actual user
|
||||||
|
pub user_id: Uuid,
|
||||||
|
|
||||||
pub server_inbound_id: Uuid,
|
pub server_inbound_id: Uuid,
|
||||||
|
|
||||||
pub username: String,
|
/// Generated xray user ID (UUID for protocols like vmess/vless)
|
||||||
|
|
||||||
pub email: String,
|
|
||||||
|
|
||||||
pub xray_user_id: String,
|
pub xray_user_id: String,
|
||||||
|
|
||||||
|
/// Generated password for protocols like trojan/shadowsocks
|
||||||
|
pub password: Option<String>,
|
||||||
|
|
||||||
pub level: i32,
|
pub level: i32,
|
||||||
|
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
@@ -28,6 +31,12 @@ pub struct Model {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::user::Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "super::user::Column::Id"
|
||||||
|
)]
|
||||||
|
User,
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to = "super::server_inbound::Entity",
|
belongs_to = "super::server_inbound::Entity",
|
||||||
from = "Column::ServerInboundId",
|
from = "Column::ServerInboundId",
|
||||||
@@ -36,6 +45,12 @@ pub enum Relation {
|
|||||||
ServerInbound,
|
ServerInbound,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::User.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Related<super::server_inbound::Entity> for Entity {
|
impl Related<super::server_inbound::Entity> for Entity {
|
||||||
fn to() -> RelationDef {
|
fn to() -> RelationDef {
|
||||||
Relation::ServerInbound.def()
|
Relation::ServerInbound.def()
|
||||||
@@ -74,41 +89,46 @@ impl ActiveModelBehavior for ActiveModel {
|
|||||||
/// Inbound user creation data transfer object
|
/// Inbound user creation data transfer object
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CreateInboundUserDto {
|
pub struct CreateInboundUserDto {
|
||||||
|
pub user_id: Uuid,
|
||||||
pub server_inbound_id: Uuid,
|
pub server_inbound_id: Uuid,
|
||||||
pub username: String,
|
|
||||||
pub level: Option<i32>,
|
pub level: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateInboundUserDto {
|
impl CreateInboundUserDto {
|
||||||
/// Generate email in format: username@OutFleet
|
/// Generate UUID for xray user (for vmess/vless)
|
||||||
pub fn generate_email(&self) -> String {
|
|
||||||
format!("{}@OutFleet", self.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate UUID for xray user
|
|
||||||
pub fn generate_xray_user_id(&self) -> String {
|
pub fn generate_xray_user_id(&self) -> String {
|
||||||
Uuid::new_v4().to_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;
|
||||||
|
|
||||||
|
thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(24)
|
||||||
|
.map(char::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inbound user update data transfer object
|
/// Inbound user update data transfer object
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UpdateInboundUserDto {
|
pub struct UpdateInboundUserDto {
|
||||||
pub username: Option<String>,
|
|
||||||
pub level: Option<i32>,
|
pub level: Option<i32>,
|
||||||
pub is_active: Option<bool>,
|
pub is_active: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CreateInboundUserDto> for ActiveModel {
|
impl From<CreateInboundUserDto> for ActiveModel {
|
||||||
fn from(dto: CreateInboundUserDto) -> Self {
|
fn from(dto: CreateInboundUserDto) -> Self {
|
||||||
let email = dto.generate_email();
|
|
||||||
let xray_user_id = dto.generate_xray_user_id();
|
let xray_user_id = dto.generate_xray_user_id();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
user_id: Set(dto.user_id),
|
||||||
server_inbound_id: Set(dto.server_inbound_id),
|
server_inbound_id: Set(dto.server_inbound_id),
|
||||||
username: Set(dto.username),
|
|
||||||
email: Set(email),
|
|
||||||
xray_user_id: Set(xray_user_id),
|
xray_user_id: Set(xray_user_id),
|
||||||
|
password: Set(Some(dto.generate_password())), // Generate password for all protocols
|
||||||
level: Set(dto.level.unwrap_or(0)),
|
level: Set(dto.level.unwrap_or(0)),
|
||||||
is_active: Set(true),
|
is_active: Set(true),
|
||||||
..Self::new()
|
..Self::new()
|
||||||
@@ -121,11 +141,6 @@ impl Model {
|
|||||||
pub fn apply_update(self, dto: UpdateInboundUserDto) -> ActiveModel {
|
pub fn apply_update(self, dto: UpdateInboundUserDto) -> ActiveModel {
|
||||||
let mut active_model: ActiveModel = self.into();
|
let mut active_model: ActiveModel = self.into();
|
||||||
|
|
||||||
if let Some(username) = dto.username {
|
|
||||||
let new_email = format!("{}@OutFleet", username);
|
|
||||||
active_model.username = Set(username);
|
|
||||||
active_model.email = Set(new_email);
|
|
||||||
}
|
|
||||||
if let Some(level) = dto.level {
|
if let Some(level) = dto.level {
|
||||||
active_model.level = Set(level);
|
active_model.level = Set(level);
|
||||||
}
|
}
|
||||||
@@ -135,16 +150,21 @@ impl Model {
|
|||||||
|
|
||||||
active_model
|
active_model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate email for xray client based on user information
|
||||||
|
pub fn generate_client_email(&self, username: &str) -> String {
|
||||||
|
format!("{}@OutFleet", username)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response model for inbound user
|
/// Response model for inbound user
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct InboundUserResponse {
|
pub struct InboundUserResponse {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
pub server_inbound_id: Uuid,
|
pub server_inbound_id: Uuid,
|
||||||
pub username: String,
|
|
||||||
pub email: String,
|
|
||||||
pub xray_user_id: String,
|
pub xray_user_id: String,
|
||||||
|
pub password: Option<String>,
|
||||||
pub level: i32,
|
pub level: i32,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
@@ -155,10 +175,10 @@ impl From<Model> for InboundUserResponse {
|
|||||||
fn from(model: Model) -> Self {
|
fn from(model: Model) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
|
user_id: model.user_id,
|
||||||
server_inbound_id: model.server_inbound_id,
|
server_inbound_id: model.server_inbound_id,
|
||||||
username: model.username,
|
|
||||||
email: model.email,
|
|
||||||
xray_user_id: model.xray_user_id,
|
xray_user_id: model.xray_user_id,
|
||||||
|
password: model.password,
|
||||||
level: model.level,
|
level: model.level,
|
||||||
is_active: model.is_active,
|
is_active: model.is_active,
|
||||||
created_at: model.created_at.to_rfc3339(),
|
created_at: model.created_at.to_rfc3339(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod m20241201_000004_create_servers_table;
|
|||||||
mod m20241201_000005_create_server_inbounds_table;
|
mod m20241201_000005_create_server_inbounds_table;
|
||||||
mod m20241201_000006_create_user_access_table;
|
mod m20241201_000006_create_user_access_table;
|
||||||
mod m20241201_000007_create_inbound_users_table;
|
mod m20241201_000007_create_inbound_users_table;
|
||||||
|
mod m20250919_000001_update_inbound_users_schema;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20241201_000005_create_server_inbounds_table::Migration),
|
Box::new(m20241201_000005_create_server_inbounds_table::Migration),
|
||||||
Box::new(m20241201_000006_create_user_access_table::Migration),
|
Box::new(m20241201_000006_create_user_access_table::Migration),
|
||||||
Box::new(m20241201_000007_create_inbound_users_table::Migration),
|
Box::new(m20241201_000007_create_inbound_users_table::Migration),
|
||||||
|
Box::new(m20250919_000001_update_inbound_users_schema::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,23 +44,23 @@ impl InboundUsersRepository {
|
|||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find user by username and inbound (for uniqueness check)
|
/// Find user by user_id and inbound (for uniqueness check - one user per inbound)
|
||||||
pub async fn find_by_username_and_inbound(&self, username: &str, 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()
|
let user = Entity::find()
|
||||||
.filter(Column::Username.eq(username))
|
.filter(Column::UserId.eq(user_id))
|
||||||
.filter(Column::ServerInboundId.eq(inbound_id))
|
.filter(Column::ServerInboundId.eq(inbound_id))
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find user by email
|
/// Find all inbound access for a specific user
|
||||||
pub async fn find_by_email(&self, email: &str) -> Result<Option<Model>> {
|
pub async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<Model>> {
|
||||||
let user = Entity::find()
|
let users = Entity::find()
|
||||||
.filter(Column::Email.eq(email))
|
.filter(Column::UserId.eq(user_id))
|
||||||
.one(&self.db)
|
.all(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(user)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(&self, dto: CreateInboundUserDto) -> Result<Model> {
|
pub async fn create(&self, dto: CreateInboundUserDto) -> Result<Model> {
|
||||||
@@ -124,9 +124,9 @@ impl InboundUsersRepository {
|
|||||||
Ok(result.rows_affected)
|
Ok(result.rows_affected)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if username already exists on this inbound
|
/// Check if user already has access to this inbound
|
||||||
pub async fn username_exists_on_inbound(&self, username: &str, 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_username_and_inbound(username, inbound_id).await?;
|
let exists = self.find_by_user_and_inbound(user_id, inbound_id).await?;
|
||||||
Ok(exists.is_some())
|
Ok(exists.is_some())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,11 @@ impl UserRepository {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find user by ID (alias for get_by_id)
|
||||||
|
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
|
||||||
|
self.get_by_id(id).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Get user by telegram ID
|
/// Get user by telegram ID
|
||||||
pub async fn get_by_telegram_id(&self, telegram_id: i64) -> Result<Option<Model>> {
|
pub async fn get_by_telegram_id(&self, telegram_id: i64) -> Result<Option<Model>> {
|
||||||
let user = User::find()
|
let user = User::find()
|
||||||
|
|||||||
20
src/main.rs
20
src/main.rs
@@ -18,7 +18,6 @@ async fn main() -> Result<()> {
|
|||||||
// Initialize logging early with basic configuration
|
// Initialize logging early with basic configuration
|
||||||
init_logging(&args.log_level.as_deref().unwrap_or("info"))?;
|
init_logging(&args.log_level.as_deref().unwrap_or("info"))?;
|
||||||
|
|
||||||
tracing::info!("Starting Xray Admin Panel v{}", env!("CARGO_PKG_VERSION"));
|
|
||||||
|
|
||||||
// Handle special flags
|
// Handle special flags
|
||||||
if args.print_default_config {
|
if args.print_default_config {
|
||||||
@@ -29,7 +28,6 @@ async fn main() -> Result<()> {
|
|||||||
// Load configuration
|
// Load configuration
|
||||||
let config = match AppConfig::load() {
|
let config = match AppConfig::load() {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
tracing::info!("Configuration loaded successfully");
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -37,14 +35,12 @@ async fn main() -> Result<()> {
|
|||||||
if args.validate_config {
|
if args.validate_config {
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
tracing::warn!("Using default configuration");
|
|
||||||
AppConfig::default()
|
AppConfig::default()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate configuration if requested
|
// Validate configuration if requested
|
||||||
if args.validate_config {
|
if args.validate_config {
|
||||||
tracing::info!("Configuration validation passed");
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +54,8 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
|
|
||||||
// Initialize database connection
|
// Initialize database connection
|
||||||
tracing::info!("Initializing database connection...");
|
|
||||||
let db = match DatabaseManager::new(&config.database).await {
|
let db = match DatabaseManager::new(&config.database).await {
|
||||||
Ok(db) => {
|
Ok(db) => {
|
||||||
tracing::info!("Database initialized successfully");
|
|
||||||
db
|
db
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -72,19 +66,13 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
// Perform database health check
|
// Perform database health check
|
||||||
match db.health_check().await {
|
match db.health_check().await {
|
||||||
Ok(true) => tracing::info!("Database health check passed"),
|
|
||||||
Ok(false) => tracing::warn!("Database health check failed"),
|
Ok(false) => tracing::warn!("Database health check failed"),
|
||||||
Err(e) => tracing::error!("Database health check error: {}", e),
|
Err(e) => tracing::error!("Database health check error: {}", e),
|
||||||
}
|
_ => {}
|
||||||
|
|
||||||
// Get schema version
|
|
||||||
if let Ok(Some(version)) = db.get_schema_version().await {
|
|
||||||
tracing::info!("Database schema version: {}", version);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize event bus first
|
// Initialize event bus first
|
||||||
let event_receiver = crate::services::events::init_event_bus();
|
let event_receiver = crate::services::events::init_event_bus();
|
||||||
tracing::info!("Event bus initialized");
|
|
||||||
|
|
||||||
// Initialize xray service
|
// Initialize xray service
|
||||||
let xray_service = XrayService::new();
|
let xray_service = XrayService::new();
|
||||||
@@ -92,24 +80,20 @@ async fn main() -> Result<()> {
|
|||||||
// Initialize and start task scheduler with dependencies
|
// Initialize and start task scheduler with dependencies
|
||||||
let mut task_scheduler = TaskScheduler::new().await?;
|
let mut task_scheduler = TaskScheduler::new().await?;
|
||||||
task_scheduler.start(db.clone(), xray_service).await?;
|
task_scheduler.start(db.clone(), xray_service).await?;
|
||||||
tracing::info!("Task scheduler started with xray sync");
|
|
||||||
|
|
||||||
// Start event-driven sync handler with the receiver
|
// Start event-driven sync handler with the receiver
|
||||||
TaskScheduler::start_event_handler(db.clone(), event_receiver).await;
|
TaskScheduler::start_event_handler(db.clone(), event_receiver).await;
|
||||||
tracing::info!("Event-driven sync handler started");
|
|
||||||
|
|
||||||
// Start web server with task scheduler
|
// Start web server with task scheduler
|
||||||
tracing::info!("Starting web server on {}:{}", config.web.host, config.web.port);
|
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
result = web::start_server(db, config.web.clone()) => {
|
result = web::start_server(db, config.web.clone()) => {
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => tracing::info!("Web server stopped gracefully"),
|
|
||||||
Err(e) => tracing::error!("Web server error: {}", e),
|
Err(e) => tracing::error!("Web server error: {}", e),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = tokio::signal::ctrl_c() => {
|
_ = tokio::signal::ctrl_c() => {
|
||||||
tracing::info!("Shutdown signal received, stopping services...");
|
|
||||||
if let Err(e) = task_scheduler.shutdown().await {
|
if let Err(e) = task_scheduler.shutdown().await {
|
||||||
tracing::error!("Error shutting down task scheduler: {}", e);
|
tracing::error!("Error shutting down task scheduler: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
|||||||
use tokio_cron_scheduler::{JobScheduler, Job};
|
use tokio_cron_scheduler::{JobScheduler, Job};
|
||||||
use tracing::{info, error, warn};
|
use tracing::{info, error, warn};
|
||||||
use crate::database::DatabaseManager;
|
use crate::database::DatabaseManager;
|
||||||
use crate::database::repository::{ServerRepository, ServerInboundRepository, InboundTemplateRepository, InboundUsersRepository, CertificateRepository};
|
use crate::database::repository::{ServerRepository, ServerInboundRepository, InboundTemplateRepository, InboundUsersRepository, CertificateRepository, UserRepository};
|
||||||
use crate::database::entities::inbound_users;
|
use crate::database::entities::inbound_users;
|
||||||
use crate::services::XrayService;
|
use crate::services::XrayService;
|
||||||
use crate::services::events::SyncEvent;
|
use crate::services::events::SyncEvent;
|
||||||
@@ -60,17 +60,12 @@ impl TaskScheduler {
|
|||||||
let xray_service = XrayService::new();
|
let xray_service = XrayService::new();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
info!("Starting event-driven sync handler");
|
|
||||||
|
|
||||||
while let Ok(event) = event_receiver.recv().await {
|
while let Ok(event) = event_receiver.recv().await {
|
||||||
match event {
|
match event {
|
||||||
SyncEvent::InboundChanged(server_id) | SyncEvent::UserAccessChanged(server_id) => {
|
SyncEvent::InboundChanged(server_id) | SyncEvent::UserAccessChanged(server_id) => {
|
||||||
info!("Received sync event for server {}", server_id);
|
|
||||||
|
|
||||||
if let Err(e) = sync_single_server_by_id(&xray_service, &db, server_id).await {
|
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);
|
error!("Failed to sync server {} from event: {}", server_id, e);
|
||||||
} else {
|
|
||||||
info!("Successfully synced server {} from event", server_id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +74,6 @@ impl TaskScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&mut self, db: DatabaseManager, xray_service: XrayService) -> Result<()> {
|
pub async fn start(&mut self, db: DatabaseManager, xray_service: XrayService) -> Result<()> {
|
||||||
info!("Starting task scheduler with database synchronization");
|
|
||||||
|
|
||||||
// Initialize task status
|
// Initialize task status
|
||||||
{
|
{
|
||||||
@@ -100,7 +94,6 @@ impl TaskScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run initial sync on startup
|
// Run initial sync on startup
|
||||||
info!("Running initial xray synchronization on startup");
|
|
||||||
let start_time = Utc::now();
|
let start_time = Utc::now();
|
||||||
self.update_task_status("xray_sync", TaskState::Running, None);
|
self.update_task_status("xray_sync", TaskState::Running, None);
|
||||||
|
|
||||||
@@ -108,7 +101,6 @@ impl TaskScheduler {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||||
self.update_task_status("xray_sync", TaskState::Success, Some(duration));
|
self.update_task_status("xray_sync", TaskState::Success, Some(duration));
|
||||||
info!("Initial xray sync completed successfully");
|
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||||
@@ -128,7 +120,6 @@ impl TaskScheduler {
|
|||||||
let task_status = task_status_clone.clone();
|
let task_status = task_status_clone.clone();
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
info!("Running scheduled xray synchronization");
|
|
||||||
let start_time = Utc::now();
|
let start_time = Utc::now();
|
||||||
|
|
||||||
// Update status to running
|
// Update status to running
|
||||||
@@ -152,7 +143,6 @@ impl TaskScheduler {
|
|||||||
task.last_duration_ms = Some(duration);
|
task.last_duration_ms = Some(duration);
|
||||||
task.last_error = None;
|
task.last_error = None;
|
||||||
}
|
}
|
||||||
info!("Scheduled xray sync completed successfully in {}ms", duration);
|
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
let duration = (Utc::now() - start_time).num_milliseconds() as u64;
|
||||||
@@ -171,7 +161,6 @@ impl TaskScheduler {
|
|||||||
|
|
||||||
self.scheduler.add(sync_job).await?;
|
self.scheduler.add(sync_job).await?;
|
||||||
|
|
||||||
info!("Task scheduler started with sync job running every minute");
|
|
||||||
|
|
||||||
self.scheduler.start().await?;
|
self.scheduler.start().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -202,7 +191,6 @@ impl TaskScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn shutdown(&mut self) -> Result<()> {
|
pub async fn shutdown(&mut self) -> Result<()> {
|
||||||
info!("Shutting down task scheduler");
|
|
||||||
self.scheduler.shutdown().await?;
|
self.scheduler.shutdown().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -210,7 +198,6 @@ impl TaskScheduler {
|
|||||||
|
|
||||||
/// Synchronize xray server state with database state
|
/// Synchronize xray server state with database state
|
||||||
async fn sync_xray_state(db: DatabaseManager, xray_service: XrayService) -> Result<()> {
|
async fn sync_xray_state(db: DatabaseManager, xray_service: XrayService) -> Result<()> {
|
||||||
info!("Starting xray state synchronization");
|
|
||||||
|
|
||||||
let server_repo = ServerRepository::new(db.connection().clone());
|
let server_repo = ServerRepository::new(db.connection().clone());
|
||||||
let inbound_repo = ServerInboundRepository::new(db.connection().clone());
|
let inbound_repo = ServerInboundRepository::new(db.connection().clone());
|
||||||
@@ -225,18 +212,13 @@ async fn sync_xray_state(db: DatabaseManager, xray_service: XrayService) -> Resu
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Found {} servers to synchronize", servers.len());
|
|
||||||
|
|
||||||
for server in servers {
|
for server in servers {
|
||||||
info!("Synchronizing server: {} ({}:{})", server.name, server.hostname, server.grpc_port);
|
|
||||||
|
|
||||||
let endpoint = format!("{}:{}", server.hostname, server.grpc_port);
|
let endpoint = format!("{}:{}", server.hostname, server.grpc_port);
|
||||||
|
|
||||||
// Test connection first
|
// Test connection first
|
||||||
match xray_service.test_connection(server.id, &endpoint).await {
|
match xray_service.test_connection(server.id, &endpoint).await {
|
||||||
Ok(true) => {
|
|
||||||
info!("Connection to server {} successful", server.name);
|
|
||||||
},
|
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
warn!("Cannot connect to server {} at {}, skipping", server.name, endpoint);
|
warn!("Cannot connect to server {} at {}, skipping", server.name, endpoint);
|
||||||
continue;
|
continue;
|
||||||
@@ -245,6 +227,7 @@ async fn sync_xray_state(db: DatabaseManager, xray_service: XrayService) -> Resu
|
|||||||
error!("Error testing connection to server {}: {}", server.name, e);
|
error!("Error testing connection to server {}: {}", server.name, e);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get desired inbounds from database
|
// Get desired inbounds from database
|
||||||
@@ -256,7 +239,6 @@ async fn sync_xray_state(db: DatabaseManager, xray_service: XrayService) -> Resu
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Server {}: desired={} inbounds", server.name, desired_inbounds.len());
|
|
||||||
|
|
||||||
// Synchronize inbounds
|
// Synchronize inbounds
|
||||||
if let Err(e) = sync_server_inbounds(
|
if let Err(e) = sync_server_inbounds(
|
||||||
@@ -266,12 +248,9 @@ async fn sync_xray_state(db: DatabaseManager, xray_service: XrayService) -> Resu
|
|||||||
&desired_inbounds
|
&desired_inbounds
|
||||||
).await {
|
).await {
|
||||||
error!("Failed to sync inbounds for server {}: {}", server.name, e);
|
error!("Failed to sync inbounds for server {}: {}", server.name, e);
|
||||||
} else {
|
|
||||||
info!("Successfully synchronized server {}", server.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Xray state synchronization completed");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +262,6 @@ async fn get_desired_inbounds_from_db(
|
|||||||
inbound_repo: &ServerInboundRepository,
|
inbound_repo: &ServerInboundRepository,
|
||||||
template_repo: &InboundTemplateRepository,
|
template_repo: &InboundTemplateRepository,
|
||||||
) -> Result<HashMap<String, DesiredInbound>> {
|
) -> Result<HashMap<String, DesiredInbound>> {
|
||||||
info!("Getting desired inbounds for server {} from database", server.name);
|
|
||||||
|
|
||||||
// Get all inbounds for this server
|
// Get all inbounds for this server
|
||||||
let inbounds = inbound_repo.find_by_server_id(server.id).await?;
|
let inbounds = inbound_repo.find_by_server_id(server.id).await?;
|
||||||
@@ -302,7 +280,6 @@ async fn get_desired_inbounds_from_db(
|
|||||||
// Get users for this inbound
|
// Get users for this inbound
|
||||||
let users = get_users_for_inbound(db, inbound.id).await?;
|
let users = get_users_for_inbound(db, inbound.id).await?;
|
||||||
|
|
||||||
info!("Inbound {}: {} users found", inbound.tag, users.len());
|
|
||||||
|
|
||||||
// Get port from template or override
|
// Get port from template or override
|
||||||
let port = inbound.port_override.unwrap_or(template.default_port);
|
let port = inbound.port_override.unwrap_or(template.default_port);
|
||||||
@@ -334,7 +311,6 @@ async fn get_desired_inbounds_from_db(
|
|||||||
desired_inbounds.insert(inbound.tag.clone(), desired_inbound);
|
desired_inbounds.insert(inbound.tag.clone(), desired_inbound);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Found {} desired inbounds for server {}", desired_inbounds.len(), server.name);
|
|
||||||
Ok(desired_inbounds)
|
Ok(desired_inbounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,13 +320,20 @@ async fn get_users_for_inbound(db: &DatabaseManager, inbound_id: Uuid) -> Result
|
|||||||
|
|
||||||
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?;
|
||||||
|
|
||||||
let users: Vec<XrayUser> = inbound_users.into_iter().map(|user| {
|
// Get user details to generate emails
|
||||||
XrayUser {
|
let user_repo = UserRepository::new(db.connection().clone());
|
||||||
id: user.xray_user_id,
|
|
||||||
email: user.email,
|
let mut users: Vec<XrayUser> = Vec::new();
|
||||||
level: user.level,
|
for inbound_user in inbound_users {
|
||||||
|
if let Some(user) = user_repo.find_by_id(inbound_user.user_id).await? {
|
||||||
|
let email = inbound_user.generate_client_email(&user.name);
|
||||||
|
users.push(XrayUser {
|
||||||
|
id: inbound_user.xray_user_id,
|
||||||
|
email,
|
||||||
|
level: inbound_user.level,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).collect();
|
}
|
||||||
|
|
||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
@@ -366,7 +349,6 @@ async fn load_certificate_from_db(db: &DatabaseManager, cert_id: Option<Uuid>) -
|
|||||||
|
|
||||||
match cert_repo.find_by_id(cert_id).await? {
|
match cert_repo.find_by_id(cert_id).await? {
|
||||||
Some(cert) => {
|
Some(cert) => {
|
||||||
info!("Loaded certificate: {}", cert.domain);
|
|
||||||
Ok((Some(cert.certificate_pem()), Some(cert.private_key_pem())))
|
Ok((Some(cert.certificate_pem()), Some(cert.private_key_pem())))
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
@@ -387,13 +369,9 @@ async fn sync_server_inbounds(
|
|||||||
// Create or update inbounds
|
// Create or update inbounds
|
||||||
// Since xray has no API to list inbounds, we always recreate them
|
// Since xray has no API to list inbounds, we always recreate them
|
||||||
for (tag, desired) in desired_inbounds {
|
for (tag, desired) in desired_inbounds {
|
||||||
info!("Creating/updating inbound: {} with {} users", tag, desired.users.len());
|
|
||||||
|
|
||||||
// Always try to remove inbound first (ignore errors if it doesn't exist)
|
// Always try to remove inbound first (ignore errors if it doesn't exist)
|
||||||
if let Err(e) = xray_service.remove_inbound(server_id, endpoint, tag).await {
|
let _ = xray_service.remove_inbound(server_id, endpoint, tag).await;
|
||||||
// Log but don't fail - inbound might not exist
|
|
||||||
info!("Inbound {} removal result: {} (this is normal if inbound didn't exist)", tag, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create inbound with users
|
// Create inbound with users
|
||||||
let users_json: Vec<Value> = desired.users.iter().map(|user| {
|
let users_json: Vec<Value> = desired.users.iter().map(|user| {
|
||||||
@@ -416,12 +394,10 @@ async fn sync_server_inbounds(
|
|||||||
desired.cert_pem.as_deref(),
|
desired.cert_pem.as_deref(),
|
||||||
desired.key_pem.as_deref(),
|
desired.key_pem.as_deref(),
|
||||||
).await {
|
).await {
|
||||||
Ok(_) => {
|
|
||||||
info!("Successfully created inbound {} with {} users", tag, desired.users.len());
|
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to create inbound {}: {}", tag, e);
|
error!("Failed to create inbound {}: {}", tag, e);
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use uuid;
|
||||||
use xray_core::{
|
use xray_core::{
|
||||||
tonic::Request,
|
tonic::Request,
|
||||||
app::proxyman::command::{AddInboundRequest, RemoveInboundRequest},
|
app::proxyman::command::{AddInboundRequest, RemoveInboundRequest},
|
||||||
@@ -7,7 +8,7 @@ use xray_core::{
|
|||||||
common::serial::TypedMessage,
|
common::serial::TypedMessage,
|
||||||
common::protocol::User,
|
common::protocol::User,
|
||||||
app::proxyman::ReceiverConfig,
|
app::proxyman::ReceiverConfig,
|
||||||
common::net::{PortList, PortRange, IpOrDomain, ip_or_domain::Address},
|
common::net::{PortList, PortRange, IpOrDomain, ip_or_domain::Address, Network},
|
||||||
transport::internet::StreamConfig,
|
transport::internet::StreamConfig,
|
||||||
transport::internet::tls::{Config as TlsConfig, Certificate as TlsCertificate},
|
transport::internet::tls::{Config as TlsConfig, Certificate as TlsCertificate},
|
||||||
proxy::vless::inbound::Config as VlessInboundConfig,
|
proxy::vless::inbound::Config as VlessInboundConfig,
|
||||||
@@ -17,7 +18,7 @@ use xray_core::{
|
|||||||
proxy::trojan::ServerConfig as TrojanServerConfig,
|
proxy::trojan::ServerConfig as TrojanServerConfig,
|
||||||
proxy::trojan::Account as TrojanAccount,
|
proxy::trojan::Account as TrojanAccount,
|
||||||
proxy::shadowsocks::ServerConfig as ShadowsocksServerConfig,
|
proxy::shadowsocks::ServerConfig as ShadowsocksServerConfig,
|
||||||
proxy::shadowsocks::Account as ShadowsocksAccount,
|
proxy::shadowsocks::{Account as ShadowsocksAccount, CipherType},
|
||||||
Client,
|
Client,
|
||||||
prost_types,
|
prost_types,
|
||||||
};
|
};
|
||||||
@@ -32,8 +33,7 @@ fn pem_to_der(pem_data: &str) -> Result<Vec<u8>> {
|
|||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
tracing::debug!("Base64 data length: {}", base64_data.len());
|
tracing::debug!("PEM to DER conversion: {} bytes", base64_data.len());
|
||||||
tracing::debug!("Base64 data: {}", &base64_data[..std::cmp::min(100, base64_data.len())]);
|
|
||||||
|
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
general_purpose::STANDARD.decode(&base64_data)
|
general_purpose::STANDARD.decode(&base64_data)
|
||||||
@@ -57,14 +57,11 @@ impl<'a> InboundClient<'a> {
|
|||||||
|
|
||||||
/// Add inbound configuration with TLS certificate and users
|
/// 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<()> {
|
||||||
tracing::info!("Adding inbound to Xray server at {}", self.endpoint);
|
|
||||||
tracing::debug!("Inbound config: {}", serde_json::to_string_pretty(inbound)?);
|
|
||||||
|
|
||||||
let tag = inbound["tag"].as_str().unwrap_or("").to_string();
|
let tag = inbound["tag"].as_str().unwrap_or("").to_string();
|
||||||
let port = inbound["port"].as_u64().unwrap_or(8080) as u32;
|
let port = inbound["port"].as_u64().unwrap_or(8080) as u32;
|
||||||
let protocol = inbound["protocol"].as_str().unwrap_or("vless");
|
let protocol = inbound["protocol"].as_str().unwrap_or("vless");
|
||||||
|
let user_count = users.map_or(0, |u| u.len());
|
||||||
|
|
||||||
tracing::debug!("Creating inbound: tag={}, port={}, protocol={}", tag, port, protocol);
|
|
||||||
|
|
||||||
// Create receiver configuration (port binding) - use simple port number
|
// Create receiver configuration (port binding) - use simple port number
|
||||||
let port_list = PortList {
|
let port_list = PortList {
|
||||||
@@ -79,8 +76,6 @@ impl<'a> InboundClient<'a> {
|
|||||||
let cert_pem = cert_pem.unwrap();
|
let cert_pem = cert_pem.unwrap();
|
||||||
let key_pem = key_pem.unwrap();
|
let key_pem = key_pem.unwrap();
|
||||||
|
|
||||||
tracing::info!("Creating StreamConfig with TLS like working example");
|
|
||||||
|
|
||||||
// Create TLS certificate exactly like working example - PEM content as bytes
|
// Create TLS certificate exactly like working example - PEM content as bytes
|
||||||
let tls_cert = TlsCertificate {
|
let tls_cert = TlsCertificate {
|
||||||
certificate: cert_pem.as_bytes().to_vec(), // PEM content as bytes like working example
|
certificate: cert_pem.as_bytes().to_vec(), // PEM content as bytes like working example
|
||||||
@@ -106,7 +101,7 @@ impl<'a> InboundClient<'a> {
|
|||||||
value: tls_config.encode_to_vec(),
|
value: tls_config.encode_to_vec(),
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("Created TLS config with server_name: {}, next_protocol: {:?}",
|
tracing::debug!("TLS config: server_name={}, protocols={:?}",
|
||||||
tls_config.server_name, tls_config.next_protocol);
|
tls_config.server_name, tls_config.next_protocol);
|
||||||
|
|
||||||
// Create StreamConfig like working example
|
// Create StreamConfig like working example
|
||||||
@@ -120,7 +115,6 @@ impl<'a> InboundClient<'a> {
|
|||||||
socket_settings: None,
|
socket_settings: None,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("No certificates provided, creating inbound without TLS");
|
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,18 +180,31 @@ impl<'a> InboundClient<'a> {
|
|||||||
let email = user["email"].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;
|
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() {
|
if !user_id.is_empty() && !email.is_empty() {
|
||||||
let account = VmessAccount {
|
let account = VmessAccount {
|
||||||
id: user_id,
|
id: user_id.clone(),
|
||||||
security_settings: None,
|
security_settings: None,
|
||||||
tests_enabled: "".to_string(),
|
tests_enabled: "".to_string(), // Keep empty as in examples
|
||||||
};
|
};
|
||||||
|
let account_bytes = account.encode_to_vec();
|
||||||
|
|
||||||
vmess_users.push(User {
|
vmess_users.push(User {
|
||||||
email,
|
email: email.clone(),
|
||||||
level,
|
level,
|
||||||
account: Some(TypedMessage {
|
account: Some(TypedMessage {
|
||||||
r#type: "xray.proxy.vmess.Account".to_string(),
|
r#type: "xray.proxy.vmess.Account".to_string(),
|
||||||
value: account.encode_to_vec(),
|
value: account_bytes,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -255,14 +262,15 @@ impl<'a> InboundClient<'a> {
|
|||||||
let email = user["email"].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;
|
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
|
||||||
if !password.is_empty() && !email.is_empty() {
|
if !password.is_empty() && !email.is_empty() {
|
||||||
let account = ShadowsocksAccount {
|
let account = ShadowsocksAccount {
|
||||||
password,
|
password,
|
||||||
cipher_type: 0, // Default cipher
|
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 {
|
ss_users.push(User {
|
||||||
email,
|
email: email.clone(),
|
||||||
level,
|
level,
|
||||||
account: Some(TypedMessage {
|
account: Some(TypedMessage {
|
||||||
r#type: "xray.proxy.shadowsocks.Account".to_string(),
|
r#type: "xray.proxy.shadowsocks.Account".to_string(),
|
||||||
@@ -275,7 +283,7 @@ impl<'a> InboundClient<'a> {
|
|||||||
|
|
||||||
let shadowsocks_config = ShadowsocksServerConfig {
|
let shadowsocks_config = ShadowsocksServerConfig {
|
||||||
users: ss_users,
|
users: ss_users,
|
||||||
network: vec![], // Support all networks by default
|
network: vec![Network::Tcp as i32, Network::Udp as i32], // Support TCP and UDP
|
||||||
};
|
};
|
||||||
TypedMessage {
|
TypedMessage {
|
||||||
r#type: "xray.proxy.shadowsocks.ServerConfig".to_string(),
|
r#type: "xray.proxy.shadowsocks.ServerConfig".to_string(),
|
||||||
@@ -293,21 +301,17 @@ impl<'a> InboundClient<'a> {
|
|||||||
proxy_settings: Some(proxy_message),
|
proxy_settings: Some(proxy_message),
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("Sending AddInboundRequest for '{}'", tag);
|
|
||||||
tracing::debug!("InboundConfig: {:?}", inbound_config);
|
|
||||||
|
|
||||||
let request = Request::new(AddInboundRequest {
|
let request = Request::new(AddInboundRequest {
|
||||||
inbound: Some(inbound_config),
|
inbound: Some(inbound_config),
|
||||||
});
|
});
|
||||||
let mut handler_client = self.client.handler();
|
let mut handler_client = self.client.handler();
|
||||||
match handler_client.add_inbound(request).await {
|
match handler_client.add_inbound(request).await {
|
||||||
Ok(response) => {
|
Ok(_) => {
|
||||||
let _response_inner = response.into_inner();
|
tracing::info!("Added {} inbound '{}' successfully", protocol, tag);
|
||||||
tracing::info!("Successfully added inbound {}", tag);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to add inbound {}: {}", tag, e);
|
tracing::error!("Failed to add {} inbound '{}': {}", protocol, tag, e);
|
||||||
Err(anyhow!("Failed to add inbound {}: {}", tag, e))
|
Err(anyhow!("Failed to add inbound {}: {}", tag, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,8 +319,6 @@ impl<'a> InboundClient<'a> {
|
|||||||
|
|
||||||
/// Remove inbound by tag
|
/// Remove inbound by tag
|
||||||
pub async fn remove_inbound(&self, tag: &str) -> Result<()> {
|
pub async fn remove_inbound(&self, tag: &str) -> Result<()> {
|
||||||
tracing::info!("Removing inbound '{}' from Xray server at {}", tag, self.endpoint);
|
|
||||||
|
|
||||||
let mut handler_client = self.client.handler();
|
let mut handler_client = self.client.handler();
|
||||||
let request = Request::new(RemoveInboundRequest {
|
let request = Request::new(RemoveInboundRequest {
|
||||||
tag: tag.to_string(),
|
tag: tag.to_string(),
|
||||||
@@ -324,11 +326,11 @@ impl<'a> InboundClient<'a> {
|
|||||||
|
|
||||||
match handler_client.remove_inbound(request).await {
|
match handler_client.remove_inbound(request).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Successfully removed inbound");
|
tracing::info!("Removed inbound '{}' from {}", tag, self.endpoint);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to remove inbound: {}", e);
|
tracing::error!("Failed to remove inbound '{}': {}", tag, e);
|
||||||
Err(anyhow!("Failed to remove inbound: {}", e))
|
Err(anyhow!("Failed to remove inbound: {}", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,8 +338,7 @@ impl<'a> InboundClient<'a> {
|
|||||||
|
|
||||||
/// Restart Xray with new configuration
|
/// 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<()> {
|
||||||
tracing::info!("Restarting Xray server at {} with new config", self.endpoint);
|
tracing::debug!("Restarting Xray server at {} with new config", self.endpoint);
|
||||||
tracing::debug!("Config: {}", serde_json::to_string_pretty(&config.to_json())?);
|
|
||||||
|
|
||||||
// TODO: Implement restart with config using xray-core
|
// TODO: Implement restart with config using xray-core
|
||||||
// For now just return success
|
// For now just return success
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ impl XrayService {
|
|||||||
"streamSettings": stream_settings
|
"streamSettings": stream_settings
|
||||||
});
|
});
|
||||||
|
|
||||||
tracing::info!("Creating inbound with config: {}", inbound_config);
|
|
||||||
self.add_inbound(_server_id, endpoint, &inbound_config).await
|
self.add_inbound(_server_id, endpoint, &inbound_config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +89,6 @@ impl XrayService {
|
|||||||
"streamSettings": stream_settings
|
"streamSettings": stream_settings
|
||||||
});
|
});
|
||||||
|
|
||||||
tracing::info!("Creating inbound with TLS certificate and config: {}", inbound_config);
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +118,6 @@ impl XrayService {
|
|||||||
|
|
||||||
/// Add user to inbound by recreating the inbound with updated user list
|
/// 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<()> {
|
||||||
tracing::info!("XrayService::add_user called for server {} endpoint {} inbound_tag {}", _server_id, endpoint, inbound_tag);
|
|
||||||
tracing::warn!("Dynamic user addition via AlterInboundRequest doesn't work reliably - need to implement inbound recreation");
|
|
||||||
|
|
||||||
// TODO: Implement inbound recreation approach:
|
// TODO: Implement inbound recreation approach:
|
||||||
// 1. Get current inbound configuration from database
|
// 1. Get current inbound configuration from database
|
||||||
@@ -147,7 +143,6 @@ impl XrayService {
|
|||||||
cert_pem: Option<&str>,
|
cert_pem: Option<&str>,
|
||||||
key_pem: Option<&str>,
|
key_pem: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
tracing::info!("Creating inbound '{}' with {} users", tag, users.len());
|
|
||||||
|
|
||||||
// Build inbound configuration with users
|
// Build inbound configuration with users
|
||||||
let mut inbound_config = serde_json::json!({
|
let mut inbound_config = serde_json::json!({
|
||||||
@@ -181,7 +176,6 @@ impl XrayService {
|
|||||||
inbound_config["settings"] = settings;
|
inbound_config["settings"] = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Creating inbound with users: {}", serde_json::to_string_pretty(&inbound_config)?);
|
|
||||||
|
|
||||||
// Use the new method with users support
|
// 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
|
||||||
|
|||||||
@@ -24,16 +24,11 @@ impl<'a> UserClient<'a> {
|
|||||||
|
|
||||||
/// Add user to inbound (simple version that works)
|
/// Add user to inbound (simple version that works)
|
||||||
pub async fn add_user(&self, inbound_tag: &str, user: &Value) -> Result<()> {
|
pub async fn add_user(&self, inbound_tag: &str, user: &Value) -> Result<()> {
|
||||||
tracing::info!("Adding user to inbound '{}' on Xray server at {}", inbound_tag, self.endpoint);
|
|
||||||
tracing::debug!("User config: {}", serde_json::to_string_pretty(user)?);
|
|
||||||
|
|
||||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||||
let user_id = user["id"].as_str().unwrap_or("").to_string();
|
let user_id = user["id"].as_str().unwrap_or("").to_string();
|
||||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||||
let protocol = user["protocol"].as_str().unwrap_or("vless");
|
let protocol = user["protocol"].as_str().unwrap_or("vless");
|
||||||
|
|
||||||
tracing::info!("Parsed user data: email={}, id={}, level={}, protocol={}", email, user_id, level, protocol);
|
|
||||||
|
|
||||||
if email.is_empty() || user_id.is_empty() {
|
if email.is_empty() || user_id.is_empty() {
|
||||||
return Err(anyhow!("User email and id are required"));
|
return Err(anyhow!("User email and id are required"));
|
||||||
}
|
}
|
||||||
@@ -99,13 +94,11 @@ impl<'a> UserClient<'a> {
|
|||||||
operation: Some(typed_message),
|
operation: Some(typed_message),
|
||||||
});
|
});
|
||||||
|
|
||||||
tracing::info!("Sending AlterInboundRequest to add user '{}' to inbound '{}'", email, inbound_tag);
|
|
||||||
|
|
||||||
let mut handler_client = self.client.handler();
|
let mut handler_client = self.client.handler();
|
||||||
match handler_client.alter_inbound(request).await {
|
match handler_client.alter_inbound(request).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let _response_inner = response.into_inner();
|
let _response_inner = response.into_inner();
|
||||||
tracing::info!("Successfully added user '{}' to inbound '{}'", email, inbound_tag);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -118,7 +111,6 @@ impl<'a> UserClient<'a> {
|
|||||||
|
|
||||||
/// Remove user from inbound
|
/// Remove user from inbound
|
||||||
pub async fn remove_user(&self, inbound_tag: &str, email: &str) -> Result<()> {
|
pub async fn remove_user(&self, inbound_tag: &str, email: &str) -> Result<()> {
|
||||||
tracing::info!("Removing user '{}' from inbound '{}' on Xray server at {}", email, inbound_tag, self.endpoint);
|
|
||||||
|
|
||||||
// Build the RemoveUserOperation
|
// Build the RemoveUserOperation
|
||||||
let remove_user_op = RemoveUserOperation {
|
let remove_user_op = RemoveUserOperation {
|
||||||
@@ -138,7 +130,6 @@ impl<'a> UserClient<'a> {
|
|||||||
let mut handler_client = self.client.handler();
|
let mut handler_client = self.client.handler();
|
||||||
match handler_client.alter_inbound(request).await {
|
match handler_client.alter_inbound(request).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Successfully removed user '{}' from inbound '{}'", email, inbound_tag);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
database::{
|
database::{
|
||||||
entities::{server, server_inbound},
|
entities::{server, server_inbound},
|
||||||
repository::{ServerRepository, ServerInboundRepository, InboundTemplateRepository, CertificateRepository, InboundUsersRepository},
|
repository::{ServerRepository, ServerInboundRepository, InboundTemplateRepository, CertificateRepository, InboundUsersRepository, UserRepository},
|
||||||
},
|
},
|
||||||
web::AppState,
|
web::AppState,
|
||||||
};
|
};
|
||||||
@@ -183,7 +183,7 @@ pub async fn create_server_inbound(
|
|||||||
Path(server_id): Path<Uuid>,
|
Path(server_id): Path<Uuid>,
|
||||||
JsonExtractor(inbound_data): JsonExtractor<server_inbound::CreateServerInboundDto>,
|
JsonExtractor(inbound_data): JsonExtractor<server_inbound::CreateServerInboundDto>,
|
||||||
) -> Result<Json<server_inbound::ServerInboundResponse>, StatusCode> {
|
) -> Result<Json<server_inbound::ServerInboundResponse>, StatusCode> {
|
||||||
tracing::info!("Creating server inbound for server {}: {:?}", server_id, inbound_data);
|
tracing::debug!("Creating server inbound for server {}", server_id);
|
||||||
|
|
||||||
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
||||||
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||||
@@ -223,11 +223,10 @@ pub async fn create_server_inbound(
|
|||||||
let (cert_pem, key_pem) = if let Some(cert_id) = inbound.certificate_id {
|
let (cert_pem, key_pem) = if let Some(cert_id) = inbound.certificate_id {
|
||||||
match cert_repo.find_by_id(cert_id).await {
|
match cert_repo.find_by_id(cert_id).await {
|
||||||
Ok(Some(cert)) => {
|
Ok(Some(cert)) => {
|
||||||
tracing::info!("Using certificate {} for inbound {}", cert.domain, inbound.tag);
|
|
||||||
(Some(cert.certificate_pem()), Some(cert.private_key_pem()))
|
(Some(cert.certificate_pem()), Some(cert.private_key_pem()))
|
||||||
},
|
},
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::warn!("Certificate {} not found, creating inbound without TLS", cert_id);
|
tracing::warn!("Certificate {} not found", cert_id);
|
||||||
(None, None)
|
(None, None)
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -236,7 +235,6 @@ pub async fn create_server_inbound(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("No certificate specified for inbound {}, creating without TLS", inbound.tag);
|
|
||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -252,16 +250,16 @@ pub async fn create_server_inbound(
|
|||||||
key_pem.as_deref(),
|
key_pem.as_deref(),
|
||||||
).await {
|
).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Successfully created inbound {} on xray server {}", inbound.tag, endpoint);
|
tracing::info!("Created inbound '{}' on {}", inbound.tag, endpoint);
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to create inbound on xray server {}: {}", 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
|
// Note: We don't fail the request since the inbound is already in DB
|
||||||
// The user can manually sync or retry later
|
// The user can manually sync or retry later
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("Inbound {} created as inactive, skipping xray server creation", inbound.tag);
|
tracing::debug!("Inbound '{}' created as inactive", inbound.tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(inbound.into()))
|
Ok(Json(inbound.into()))
|
||||||
@@ -273,7 +271,7 @@ pub async fn update_server_inbound(
|
|||||||
Path((server_id, inbound_id)): Path<(Uuid, Uuid)>,
|
Path((server_id, inbound_id)): Path<(Uuid, Uuid)>,
|
||||||
JsonExtractor(inbound_data): JsonExtractor<server_inbound::UpdateServerInboundDto>,
|
JsonExtractor(inbound_data): JsonExtractor<server_inbound::UpdateServerInboundDto>,
|
||||||
) -> Result<Json<server_inbound::ServerInboundResponse>, StatusCode> {
|
) -> Result<Json<server_inbound::ServerInboundResponse>, StatusCode> {
|
||||||
tracing::info!("Updating server inbound {} for server {}: {:?}", inbound_id, server_id, inbound_data);
|
tracing::debug!("Updating server inbound {} for server {}", inbound_id, server_id);
|
||||||
|
|
||||||
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
||||||
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
let inbound_repo = ServerInboundRepository::new(app_state.db.connection().clone());
|
||||||
@@ -303,19 +301,17 @@ pub async fn update_server_inbound(
|
|||||||
// Handle xray server changes based on active status change
|
// Handle xray server changes based on active status change
|
||||||
if old_is_active && !new_is_active {
|
if old_is_active && !new_is_active {
|
||||||
// Becoming inactive - remove from xray server
|
// Becoming inactive - remove from xray server
|
||||||
tracing::info!("Inbound {} becoming inactive, removing from xray server {}", current_inbound.tag, endpoint);
|
|
||||||
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(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Successfully removed inbound {} from xray server", current_inbound.tag);
|
tracing::info!("Deactivated inbound '{}' on {}", current_inbound.tag, endpoint);
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to remove inbound {} from xray server: {}", current_inbound.tag, e);
|
tracing::error!("Failed to deactivate inbound '{}': {}", current_inbound.tag, e);
|
||||||
// Continue with database update even if xray removal fails
|
// Continue with database update even if xray removal fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !old_is_active && new_is_active {
|
} else if !old_is_active && new_is_active {
|
||||||
// Becoming active - add to xray server
|
// Becoming active - add to xray server
|
||||||
tracing::info!("Inbound {} becoming active, adding to xray server {}", current_inbound.tag, endpoint);
|
|
||||||
|
|
||||||
// Get template info for recreation
|
// Get template info for recreation
|
||||||
let template = match template_repo.find_by_id(current_inbound.template_id).await {
|
let template = match template_repo.find_by_id(current_inbound.template_id).await {
|
||||||
@@ -332,11 +328,10 @@ pub async fn update_server_inbound(
|
|||||||
let (cert_pem, key_pem) = if let Some(cert_id) = certificate_id {
|
let (cert_pem, key_pem) = if let Some(cert_id) = certificate_id {
|
||||||
match cert_repo.find_by_id(cert_id).await {
|
match cert_repo.find_by_id(cert_id).await {
|
||||||
Ok(Some(cert)) => {
|
Ok(Some(cert)) => {
|
||||||
tracing::info!("Using certificate {} for inbound {}", cert.domain, current_inbound.tag);
|
|
||||||
(Some(cert.certificate_pem()), Some(cert.private_key_pem()))
|
(Some(cert.certificate_pem()), Some(cert.private_key_pem()))
|
||||||
},
|
},
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::warn!("Certificate {} not found, creating inbound without TLS", cert_id);
|
tracing::warn!("Certificate {} not found", cert_id);
|
||||||
(None, None)
|
(None, None)
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -345,7 +340,6 @@ pub async fn update_server_inbound(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("No certificate specified for inbound {}, creating without TLS", current_inbound.tag);
|
|
||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -361,10 +355,10 @@ pub async fn update_server_inbound(
|
|||||||
key_pem.as_deref(),
|
key_pem.as_deref(),
|
||||||
).await {
|
).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Successfully added inbound {} to xray server", current_inbound.tag);
|
tracing::info!("Activated inbound '{}' on {}", current_inbound.tag, endpoint);
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to add inbound {} to xray server: {}", current_inbound.tag, e);
|
tracing::error!("Failed to activate inbound '{}': {}", current_inbound.tag, e);
|
||||||
// Continue with database update even if xray creation fails
|
// Continue with database update even if xray creation fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,10 +422,10 @@ pub async fn delete_server_inbound(
|
|||||||
let endpoint = server.get_grpc_endpoint();
|
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(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Successfully removed inbound {} from xray server {}", inbound.tag, endpoint);
|
tracing::info!("Removed inbound '{}' from {}", inbound.tag, endpoint);
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to remove inbound from xray server {}: {}", endpoint, e);
|
tracing::error!("Failed to remove inbound '{}' from {}: {}", inbound.tag, endpoint, e);
|
||||||
// Continue with database deletion even if xray removal fails
|
// Continue with database deletion even if xray removal fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,16 +444,18 @@ pub async fn delete_server_inbound(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add user to server inbound (database only - sync will apply changes)
|
/// Give user access to server inbound (database only - sync will apply changes)
|
||||||
pub async fn add_user_to_inbound(
|
pub async fn add_user_to_inbound(
|
||||||
State(app_state): State<AppState>,
|
State(app_state): State<AppState>,
|
||||||
Path((server_id, inbound_id)): Path<(Uuid, Uuid)>,
|
Path((server_id, inbound_id)): Path<(Uuid, Uuid)>,
|
||||||
JsonExtractor(user_data): JsonExtractor<serde_json::Value>,
|
JsonExtractor(user_data): JsonExtractor<serde_json::Value>,
|
||||||
) -> Result<StatusCode, StatusCode> {
|
) -> Result<StatusCode, StatusCode> {
|
||||||
use crate::database::entities::inbound_users::CreateInboundUserDto;
|
use crate::database::entities::inbound_users::CreateInboundUserDto;
|
||||||
|
use crate::database::entities::user::CreateUserDto;
|
||||||
|
|
||||||
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
let server_repo = ServerRepository::new(app_state.db.connection().clone());
|
||||||
let inbound_repo = ServerInboundRepository::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
|
// Get server and inbound to validate they exist
|
||||||
let _server = match server_repo.find_by_id(server_id).await {
|
let _server = match server_repo.find_by_id(server_id).await {
|
||||||
@@ -479,54 +475,75 @@ pub async fn add_user_to_inbound(
|
|||||||
return Err(StatusCode::BAD_REQUEST);
|
return Err(StatusCode::BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract user data with better error handling
|
// Extract user data
|
||||||
tracing::debug!("Received user_data: {}", serde_json::to_string_pretty(&user_data).unwrap_or_else(|_| "invalid json".to_string()));
|
|
||||||
|
|
||||||
let username = user_data["username"].as_str()
|
let user_name = user_data["name"].as_str()
|
||||||
.or_else(|| user_data["email"].as_str()) // Try email as fallback
|
.or_else(|| user_data["username"].as_str())
|
||||||
.or_else(|| user_data["name"].as_str()) // Try name as fallback
|
.or_else(|| user_data["email"].as_str())
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
// Generate username if not provided
|
|
||||||
format!("user_{}", Uuid::new_v4().to_string()[..8].to_string())
|
format!("user_{}", Uuid::new_v4().to_string()[..8].to_string())
|
||||||
});
|
});
|
||||||
|
|
||||||
let level = user_data["level"].as_u64().unwrap_or(0) as i32;
|
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());
|
||||||
|
|
||||||
tracing::info!("Creating user with username: '{}' and level: {}", username, level);
|
// Get or create user
|
||||||
|
let user = if let Some(uid) = user_id {
|
||||||
|
// Use existing user
|
||||||
|
match user_repo.find_by_id(uid).await {
|
||||||
|
Ok(Some(user)) => user,
|
||||||
|
Ok(None) => return Err(StatusCode::NOT_FOUND), // User not found
|
||||||
|
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new user
|
||||||
|
let create_user_dto = CreateUserDto {
|
||||||
|
name: user_name.clone(),
|
||||||
|
comment: user_data["comment"].as_str().map(|s| s.to_string()),
|
||||||
|
telegram_id: user_data["telegram_id"].as_i64(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match user_repo.create(create_user_dto).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to create user '{}': {}", user_name, e);
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Create inbound user repository
|
// Create inbound user repository
|
||||||
let inbound_users_repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
let inbound_users_repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||||
|
|
||||||
// Check if username already exists on this inbound
|
// Check if user already has access to this inbound
|
||||||
if inbound_users_repo.username_exists_on_inbound(&username, inbound_id).await.unwrap_or(false) {
|
if inbound_users_repo.user_has_access_to_inbound(user.id, inbound_id).await.unwrap_or(false) {
|
||||||
tracing::error!("Username '{}' already exists on inbound {}", username, inbound_id);
|
tracing::warn!("User '{}' already has access to inbound", user.name);
|
||||||
return Err(StatusCode::CONFLICT);
|
return Err(StatusCode::CONFLICT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inbound user DTO
|
// Create inbound access for user
|
||||||
let inbound_user_dto = CreateInboundUserDto {
|
let inbound_user_dto = CreateInboundUserDto {
|
||||||
|
user_id: user.id,
|
||||||
server_inbound_id: inbound_id,
|
server_inbound_id: inbound_id,
|
||||||
username: username.clone(),
|
|
||||||
level: Some(level),
|
level: Some(level),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create user in database
|
// Grant access in database
|
||||||
match inbound_users_repo.create(inbound_user_dto).await {
|
match inbound_users_repo.create(inbound_user_dto).await {
|
||||||
Ok(created_user) => {
|
Ok(created_access) => {
|
||||||
tracing::info!("Inbound user created: username={} email={} server={} inbound={}",
|
tracing::info!("Granted user '{}' access to inbound (xray_id={})",
|
||||||
username, created_user.email, server_id, inbound_id);
|
user.name, created_access.xray_user_id);
|
||||||
|
|
||||||
// Send sync event for immediate synchronization
|
// Send sync event for immediate synchronization
|
||||||
crate::services::events::send_sync_event(
|
crate::services::events::send_sync_event(
|
||||||
crate::services::events::SyncEvent::UserAccessChanged(server_id)
|
crate::services::events::SyncEvent::UserAccessChanged(server_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::info!("User will be synced to xray server immediately via event");
|
|
||||||
Ok(StatusCode::CREATED)
|
Ok(StatusCode::CREATED)
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to create inbound user: {}", e);
|
tracing::error!("Failed to grant user '{}' access: {}", user.name, e);
|
||||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -571,11 +588,11 @@ pub async fn remove_user_from_inbound(
|
|||||||
// Remove user from xray server
|
// Remove user from xray server
|
||||||
match app_state.xray_service.remove_user(server_id, &format!("{}:{}", server.hostname, server.grpc_port), &inbound_tag, &email).await {
|
match app_state.xray_service.remove_user(server_id, &format!("{}:{}", server.hostname, server.grpc_port), &inbound_tag, &email).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Successfully removed user {} from server {} inbound {}", email, server_id, inbound_id);
|
tracing::info!("Removed user '{}' from inbound", email);
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to remove user {} from server {} inbound {}: {}", email, server_id, inbound_id, e);
|
tracing::error!("Failed to remove user '{}' from inbound: {}", email, e);
|
||||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user