mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-25 09:49:08 +00:00
init rust. WIP: tls for inbounds
This commit is contained in:
243
src/database/entities/certificate.rs
Normal file
243
src/database/entities/certificate.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "certificates")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[sea_orm(column_name = "cert_type")]
|
||||
pub cert_type: String,
|
||||
|
||||
pub domain: String,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub cert_data: Vec<u8>,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub key_data: Vec<u8>,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub chain_data: Option<Vec<u8>>,
|
||||
|
||||
pub expires_at: DateTimeUtc,
|
||||
|
||||
pub auto_renew: bool,
|
||||
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::server::Entity")]
|
||||
Servers,
|
||||
#[sea_orm(has_many = "super::server_inbound::Entity")]
|
||||
ServerInbounds,
|
||||
}
|
||||
|
||||
impl Related<super::server::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Servers.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server_inbound::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerInbounds.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn before_save<'life0, 'async_trait, C>(
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !insert {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum CertificateType {
|
||||
SelfSigned,
|
||||
Imported,
|
||||
}
|
||||
|
||||
impl From<CertificateType> for String {
|
||||
fn from(cert_type: CertificateType) -> Self {
|
||||
match cert_type {
|
||||
CertificateType::SelfSigned => "self_signed".to_string(),
|
||||
CertificateType::Imported => "imported".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CertificateType {
|
||||
fn from(s: String) -> Self {
|
||||
match s.as_str() {
|
||||
"self_signed" => CertificateType::SelfSigned,
|
||||
"imported" => CertificateType::Imported,
|
||||
_ => CertificateType::SelfSigned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateCertificateDto {
|
||||
pub name: String,
|
||||
pub cert_type: String,
|
||||
pub domain: String,
|
||||
pub auto_renew: bool,
|
||||
#[serde(default)]
|
||||
pub certificate_pem: String,
|
||||
#[serde(default)]
|
||||
pub private_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateCertificateDto {
|
||||
pub name: Option<String>,
|
||||
pub auto_renew: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CertificateResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub cert_type: String,
|
||||
pub domain: String,
|
||||
pub expires_at: DateTimeUtc,
|
||||
pub auto_renew: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub has_cert_data: bool,
|
||||
pub has_key_data: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CertificateDetailsResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub cert_type: String,
|
||||
pub domain: String,
|
||||
pub expires_at: DateTimeUtc,
|
||||
pub auto_renew: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub certificate_pem: String,
|
||||
pub has_private_key: bool,
|
||||
}
|
||||
|
||||
impl From<Model> for CertificateResponse {
|
||||
fn from(cert: Model) -> Self {
|
||||
Self {
|
||||
id: cert.id,
|
||||
name: cert.name,
|
||||
cert_type: cert.cert_type,
|
||||
domain: cert.domain,
|
||||
expires_at: cert.expires_at,
|
||||
auto_renew: cert.auto_renew,
|
||||
created_at: cert.created_at,
|
||||
updated_at: cert.updated_at,
|
||||
has_cert_data: !cert.cert_data.is_empty(),
|
||||
has_key_data: !cert.key_data.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Model> for CertificateDetailsResponse {
|
||||
fn from(cert: Model) -> Self {
|
||||
let certificate_pem = cert.certificate_pem();
|
||||
let has_private_key = !cert.key_data.is_empty();
|
||||
|
||||
Self {
|
||||
id: cert.id,
|
||||
name: cert.name,
|
||||
cert_type: cert.cert_type,
|
||||
domain: cert.domain,
|
||||
expires_at: cert.expires_at,
|
||||
auto_renew: cert.auto_renew,
|
||||
created_at: cert.created_at,
|
||||
updated_at: cert.updated_at,
|
||||
certificate_pem,
|
||||
has_private_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
#[allow(dead_code)]
|
||||
pub fn is_expired(&self) -> bool {
|
||||
self.expires_at < chrono::Utc::now()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn expires_soon(&self, days: i64) -> bool {
|
||||
let threshold = chrono::Utc::now() + chrono::Duration::days(days);
|
||||
self.expires_at < threshold
|
||||
}
|
||||
|
||||
/// Get certificate data as PEM string
|
||||
pub fn certificate_pem(&self) -> String {
|
||||
String::from_utf8_lossy(&self.cert_data).to_string()
|
||||
}
|
||||
|
||||
/// Get private key data as PEM string
|
||||
pub fn private_key_pem(&self) -> String {
|
||||
String::from_utf8_lossy(&self.key_data).to_string()
|
||||
}
|
||||
|
||||
pub fn apply_update(self, dto: UpdateCertificateDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
if let Some(name) = dto.name {
|
||||
active_model.name = Set(name);
|
||||
}
|
||||
if let Some(auto_renew) = dto.auto_renew {
|
||||
active_model.auto_renew = Set(auto_renew);
|
||||
}
|
||||
|
||||
active_model
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreateCertificateDto> for ActiveModel {
|
||||
fn from(dto: CreateCertificateDto) -> Self {
|
||||
Self {
|
||||
name: Set(dto.name),
|
||||
cert_type: Set(dto.cert_type),
|
||||
domain: Set(dto.domain),
|
||||
cert_data: Set(dto.certificate_pem.into_bytes()),
|
||||
key_data: Set(dto.private_key.into_bytes()),
|
||||
chain_data: Set(None),
|
||||
expires_at: Set(chrono::Utc::now() + chrono::Duration::days(90)), // Default 90 days
|
||||
auto_renew: Set(dto.auto_renew),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
278
src/database/entities/inbound_template.rs
Normal file
278
src/database/entities/inbound_template.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "inbound_templates")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
pub name: String,
|
||||
|
||||
pub description: Option<String>,
|
||||
|
||||
pub protocol: String,
|
||||
|
||||
pub default_port: i32,
|
||||
|
||||
pub base_settings: Value,
|
||||
|
||||
pub stream_settings: Value,
|
||||
|
||||
pub requires_tls: bool,
|
||||
|
||||
pub requires_domain: bool,
|
||||
|
||||
pub variables: Value,
|
||||
|
||||
pub is_active: bool,
|
||||
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::server_inbound::Entity")]
|
||||
ServerInbounds,
|
||||
}
|
||||
|
||||
impl Related<super::server_inbound::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerInbounds.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn before_save<'life0, 'async_trait, C>(
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !insert {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Protocol {
|
||||
Vless,
|
||||
Vmess,
|
||||
Trojan,
|
||||
Shadowsocks,
|
||||
}
|
||||
|
||||
impl From<Protocol> for String {
|
||||
fn from(protocol: Protocol) -> Self {
|
||||
match protocol {
|
||||
Protocol::Vless => "vless".to_string(),
|
||||
Protocol::Vmess => "vmess".to_string(),
|
||||
Protocol::Trojan => "trojan".to_string(),
|
||||
Protocol::Shadowsocks => "shadowsocks".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Protocol {
|
||||
fn from(s: String) -> Self {
|
||||
match s.as_str() {
|
||||
"vless" => Protocol::Vless,
|
||||
"vmess" => Protocol::Vmess,
|
||||
"trojan" => Protocol::Trojan,
|
||||
"shadowsocks" => Protocol::Shadowsocks,
|
||||
_ => Protocol::Vless,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TemplateVariable {
|
||||
pub key: String,
|
||||
pub var_type: VariableType,
|
||||
pub required: bool,
|
||||
pub default_value: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum VariableType {
|
||||
String,
|
||||
Number,
|
||||
Path,
|
||||
Domain,
|
||||
Port,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateInboundTemplateDto {
|
||||
pub name: String,
|
||||
pub protocol: String,
|
||||
pub default_port: i32,
|
||||
pub requires_tls: bool,
|
||||
pub config_template: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateInboundTemplateDto {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub default_port: Option<i32>,
|
||||
pub base_settings: Option<Value>,
|
||||
pub stream_settings: Option<Value>,
|
||||
pub requires_tls: Option<bool>,
|
||||
pub requires_domain: Option<bool>,
|
||||
pub variables: Option<Vec<TemplateVariable>>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InboundTemplateResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub protocol: String,
|
||||
pub default_port: i32,
|
||||
pub base_settings: Value,
|
||||
pub stream_settings: Value,
|
||||
pub requires_tls: bool,
|
||||
pub requires_domain: bool,
|
||||
pub variables: Vec<TemplateVariable>,
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
impl From<Model> for InboundTemplateResponse {
|
||||
fn from(template: Model) -> Self {
|
||||
let variables = template.get_variables();
|
||||
Self {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
protocol: template.protocol,
|
||||
default_port: template.default_port,
|
||||
base_settings: template.base_settings,
|
||||
stream_settings: template.stream_settings,
|
||||
requires_tls: template.requires_tls,
|
||||
requires_domain: template.requires_domain,
|
||||
variables,
|
||||
is_active: template.is_active,
|
||||
created_at: template.created_at,
|
||||
updated_at: template.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreateInboundTemplateDto> for ActiveModel {
|
||||
fn from(dto: CreateInboundTemplateDto) -> Self {
|
||||
// Parse config_template as JSON or use default
|
||||
let config_json: Value = serde_json::from_str(&dto.config_template)
|
||||
.unwrap_or_else(|_| serde_json::json!({}));
|
||||
|
||||
Self {
|
||||
name: Set(dto.name),
|
||||
description: Set(None),
|
||||
protocol: Set(dto.protocol),
|
||||
default_port: Set(dto.default_port),
|
||||
base_settings: Set(config_json.clone()),
|
||||
stream_settings: Set(serde_json::json!({})),
|
||||
requires_tls: Set(dto.requires_tls),
|
||||
requires_domain: Set(false),
|
||||
variables: Set(Value::Array(vec![])),
|
||||
is_active: Set(true),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn get_variables(&self) -> Vec<TemplateVariable> {
|
||||
serde_json::from_value(self.variables.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn apply_variables(&self, values: &serde_json::Map<String, Value>) -> Result<(Value, Value), String> {
|
||||
let base_settings = self.base_settings.clone();
|
||||
let stream_settings = self.stream_settings.clone();
|
||||
|
||||
// Replace variables in JSON using simple string replacement
|
||||
let base_str = base_settings.to_string();
|
||||
let stream_str = stream_settings.to_string();
|
||||
|
||||
let mut result_base = base_str;
|
||||
let mut result_stream = stream_str;
|
||||
|
||||
for (key, value) in values {
|
||||
let placeholder = format!("${{{}}}", key);
|
||||
let replacement = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
_ => value.to_string(),
|
||||
};
|
||||
result_base = result_base.replace(&placeholder, &replacement);
|
||||
result_stream = result_stream.replace(&placeholder, &replacement);
|
||||
}
|
||||
|
||||
let final_base: Value = serde_json::from_str(&result_base)
|
||||
.map_err(|e| format!("Invalid base settings after variable substitution: {}", e))?;
|
||||
let final_stream: Value = serde_json::from_str(&result_stream)
|
||||
.map_err(|e| format!("Invalid stream settings after variable substitution: {}", e))?;
|
||||
|
||||
Ok((final_base, final_stream))
|
||||
}
|
||||
|
||||
pub fn apply_update(self, dto: UpdateInboundTemplateDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
if let Some(name) = dto.name {
|
||||
active_model.name = Set(name);
|
||||
}
|
||||
if let Some(description) = dto.description {
|
||||
active_model.description = Set(Some(description));
|
||||
}
|
||||
if let Some(default_port) = dto.default_port {
|
||||
active_model.default_port = Set(default_port);
|
||||
}
|
||||
if let Some(base_settings) = dto.base_settings {
|
||||
active_model.base_settings = Set(base_settings);
|
||||
}
|
||||
if let Some(stream_settings) = dto.stream_settings {
|
||||
active_model.stream_settings = Set(stream_settings);
|
||||
}
|
||||
if let Some(requires_tls) = dto.requires_tls {
|
||||
active_model.requires_tls = Set(requires_tls);
|
||||
}
|
||||
if let Some(requires_domain) = dto.requires_domain {
|
||||
active_model.requires_domain = Set(requires_domain);
|
||||
}
|
||||
if let Some(variables) = dto.variables {
|
||||
active_model.variables = Set(serde_json::to_value(variables).unwrap_or(Value::Array(vec![])));
|
||||
}
|
||||
if let Some(is_active) = dto.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
active_model
|
||||
}
|
||||
}
|
||||
168
src/database/entities/inbound_users.rs
Normal file
168
src/database/entities/inbound_users.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "inbound_users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
pub server_inbound_id: Uuid,
|
||||
|
||||
pub username: String,
|
||||
|
||||
pub email: String,
|
||||
|
||||
pub xray_user_id: String,
|
||||
|
||||
pub level: i32,
|
||||
|
||||
pub is_active: bool,
|
||||
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::server_inbound::Entity",
|
||||
from = "Column::ServerInboundId",
|
||||
to = "super::server_inbound::Column::Id"
|
||||
)]
|
||||
ServerInbound,
|
||||
}
|
||||
|
||||
impl Related<super::server_inbound::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerInbound.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn before_save<'life0, 'async_trait, C>(
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !insert {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Inbound user creation data transfer object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateInboundUserDto {
|
||||
pub server_inbound_id: Uuid,
|
||||
pub username: String,
|
||||
pub level: Option<i32>,
|
||||
}
|
||||
|
||||
impl CreateInboundUserDto {
|
||||
/// Generate email in format: username@OutFleet
|
||||
pub fn generate_email(&self) -> String {
|
||||
format!("{}@OutFleet", self.username)
|
||||
}
|
||||
|
||||
/// Generate UUID for xray user
|
||||
pub fn generate_xray_user_id(&self) -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inbound user update data transfer object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateInboundUserDto {
|
||||
pub username: Option<String>,
|
||||
pub level: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<CreateInboundUserDto> for ActiveModel {
|
||||
fn from(dto: CreateInboundUserDto) -> Self {
|
||||
let email = dto.generate_email();
|
||||
let xray_user_id = dto.generate_xray_user_id();
|
||||
|
||||
Self {
|
||||
server_inbound_id: Set(dto.server_inbound_id),
|
||||
username: Set(dto.username),
|
||||
email: Set(email),
|
||||
xray_user_id: Set(xray_user_id),
|
||||
level: Set(dto.level.unwrap_or(0)),
|
||||
is_active: Set(true),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Update this model with data from UpdateInboundUserDto
|
||||
pub fn apply_update(self, dto: UpdateInboundUserDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
if let Some(username) = dto.username {
|
||||
let new_email = format!("{}@OutFleet", username);
|
||||
active_model.username = Set(username);
|
||||
active_model.email = Set(new_email);
|
||||
}
|
||||
if let Some(level) = dto.level {
|
||||
active_model.level = Set(level);
|
||||
}
|
||||
if let Some(is_active) = dto.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
active_model
|
||||
}
|
||||
}
|
||||
|
||||
/// Response model for inbound user
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InboundUserResponse {
|
||||
pub id: Uuid,
|
||||
pub server_inbound_id: Uuid,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub xray_user_id: String,
|
||||
pub level: i32,
|
||||
pub is_active: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<Model> for InboundUserResponse {
|
||||
fn from(model: Model) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
server_inbound_id: model.server_inbound_id,
|
||||
username: model.username,
|
||||
email: model.email,
|
||||
xray_user_id: model.xray_user_id,
|
||||
level: model.level,
|
||||
is_active: model.is_active,
|
||||
created_at: model.created_at.to_rfc3339(),
|
||||
updated_at: model.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/database/entities/mod.rs
Normal file
16
src/database/entities/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod user;
|
||||
pub mod certificate;
|
||||
pub mod inbound_template;
|
||||
pub mod server;
|
||||
pub mod server_inbound;
|
||||
pub mod user_access;
|
||||
pub mod inbound_users;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::certificate::Entity as Certificate;
|
||||
pub use super::inbound_template::Entity as InboundTemplate;
|
||||
pub use super::server::Entity as Server;
|
||||
pub use super::server_inbound::Entity as ServerInbound;
|
||||
pub use super::user_access::Entity as UserAccess;
|
||||
pub use super::inbound_users::Entity as InboundUsers;
|
||||
}
|
||||
212
src/database/entities/server.rs
Normal file
212
src/database/entities/server.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "servers")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
pub name: String,
|
||||
|
||||
pub hostname: String,
|
||||
|
||||
pub grpc_port: i32,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub api_credentials: Option<String>,
|
||||
|
||||
pub status: String,
|
||||
|
||||
pub default_certificate_id: Option<Uuid>,
|
||||
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::certificate::Entity",
|
||||
from = "Column::DefaultCertificateId",
|
||||
to = "super::certificate::Column::Id"
|
||||
)]
|
||||
DefaultCertificate,
|
||||
#[sea_orm(has_many = "super::server_inbound::Entity")]
|
||||
ServerInbounds,
|
||||
}
|
||||
|
||||
impl Related<super::certificate::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::DefaultCertificate.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server_inbound::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerInbounds.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
status: Set(ServerStatus::Unknown.into()),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn before_save<'life0, 'async_trait, C>(
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !insert {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerStatus {
|
||||
Unknown,
|
||||
Online,
|
||||
Offline,
|
||||
Error,
|
||||
Connecting,
|
||||
}
|
||||
|
||||
impl From<ServerStatus> for String {
|
||||
fn from(status: ServerStatus) -> Self {
|
||||
match status {
|
||||
ServerStatus::Unknown => "unknown".to_string(),
|
||||
ServerStatus::Online => "online".to_string(),
|
||||
ServerStatus::Offline => "offline".to_string(),
|
||||
ServerStatus::Error => "error".to_string(),
|
||||
ServerStatus::Connecting => "connecting".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ServerStatus {
|
||||
fn from(s: String) -> Self {
|
||||
match s.as_str() {
|
||||
"online" => ServerStatus::Online,
|
||||
"offline" => ServerStatus::Offline,
|
||||
"error" => ServerStatus::Error,
|
||||
"connecting" => ServerStatus::Connecting,
|
||||
_ => ServerStatus::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateServerDto {
|
||||
pub name: String,
|
||||
pub hostname: String,
|
||||
pub grpc_port: Option<i32>,
|
||||
pub api_credentials: Option<String>,
|
||||
pub default_certificate_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateServerDto {
|
||||
pub name: Option<String>,
|
||||
pub hostname: Option<String>,
|
||||
pub grpc_port: Option<i32>,
|
||||
pub api_credentials: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub default_certificate_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub hostname: String,
|
||||
pub grpc_port: i32,
|
||||
pub status: String,
|
||||
pub default_certificate_id: Option<Uuid>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub has_credentials: bool,
|
||||
}
|
||||
|
||||
impl From<CreateServerDto> for ActiveModel {
|
||||
fn from(dto: CreateServerDto) -> Self {
|
||||
Self {
|
||||
name: Set(dto.name),
|
||||
hostname: Set(dto.hostname),
|
||||
grpc_port: Set(dto.grpc_port.unwrap_or(2053)),
|
||||
api_credentials: Set(dto.api_credentials),
|
||||
status: Set("unknown".to_string()),
|
||||
default_certificate_id: Set(dto.default_certificate_id),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Model> for ServerResponse {
|
||||
fn from(server: Model) -> Self {
|
||||
Self {
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
hostname: server.hostname,
|
||||
grpc_port: server.grpc_port,
|
||||
status: server.status,
|
||||
default_certificate_id: server.default_certificate_id,
|
||||
created_at: server.created_at,
|
||||
updated_at: server.updated_at,
|
||||
has_credentials: server.api_credentials.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn apply_update(self, dto: UpdateServerDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
if let Some(name) = dto.name {
|
||||
active_model.name = Set(name);
|
||||
}
|
||||
if let Some(hostname) = dto.hostname {
|
||||
active_model.hostname = Set(hostname);
|
||||
}
|
||||
if let Some(grpc_port) = dto.grpc_port {
|
||||
active_model.grpc_port = Set(grpc_port);
|
||||
}
|
||||
if let Some(api_credentials) = dto.api_credentials {
|
||||
active_model.api_credentials = Set(Some(api_credentials));
|
||||
}
|
||||
if let Some(status) = dto.status {
|
||||
active_model.status = Set(status);
|
||||
}
|
||||
if let Some(default_certificate_id) = dto.default_certificate_id {
|
||||
active_model.default_certificate_id = Set(Some(default_certificate_id));
|
||||
}
|
||||
|
||||
active_model
|
||||
}
|
||||
|
||||
pub fn get_grpc_endpoint(&self) -> String {
|
||||
format!("{}:{}", self.hostname, self.grpc_port)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_status(&self) -> ServerStatus {
|
||||
self.status.clone().into()
|
||||
}
|
||||
}
|
||||
204
src/database/entities/server_inbound.rs
Normal file
204
src/database/entities/server_inbound.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "server_inbounds")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
pub server_id: Uuid,
|
||||
|
||||
pub template_id: Uuid,
|
||||
|
||||
pub tag: String,
|
||||
|
||||
pub port_override: Option<i32>,
|
||||
|
||||
pub certificate_id: Option<Uuid>,
|
||||
|
||||
pub variable_values: Value,
|
||||
|
||||
pub is_active: bool,
|
||||
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::server::Entity",
|
||||
from = "Column::ServerId",
|
||||
to = "super::server::Column::Id"
|
||||
)]
|
||||
Server,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::inbound_template::Entity",
|
||||
from = "Column::TemplateId",
|
||||
to = "super::inbound_template::Column::Id"
|
||||
)]
|
||||
Template,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::certificate::Entity",
|
||||
from = "Column::CertificateId",
|
||||
to = "super::certificate::Column::Id"
|
||||
)]
|
||||
Certificate,
|
||||
}
|
||||
|
||||
impl Related<super::server::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Server.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::inbound_template::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Template.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::certificate::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Certificate.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn before_save<'life0, 'async_trait, C>(
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !insert {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateServerInboundDto {
|
||||
pub template_id: Uuid,
|
||||
pub port: i32,
|
||||
pub certificate_id: Option<Uuid>,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateServerInboundDto {
|
||||
pub tag: Option<String>,
|
||||
pub port_override: Option<i32>,
|
||||
pub certificate_id: Option<Uuid>,
|
||||
pub variable_values: Option<serde_json::Map<String, Value>>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerInboundResponse {
|
||||
pub id: Uuid,
|
||||
pub server_id: Uuid,
|
||||
pub template_id: Uuid,
|
||||
pub tag: String,
|
||||
pub port: i32,
|
||||
pub certificate_id: Option<Uuid>,
|
||||
pub variable_values: Value,
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
// Populated by joins (simplified for now)
|
||||
pub template_name: Option<String>,
|
||||
pub certificate_name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Model> for ServerInboundResponse {
|
||||
fn from(inbound: Model) -> Self {
|
||||
Self {
|
||||
id: inbound.id,
|
||||
server_id: inbound.server_id,
|
||||
template_id: inbound.template_id,
|
||||
tag: inbound.tag,
|
||||
port: inbound.port_override.unwrap_or(443), // Default port if not set
|
||||
certificate_id: inbound.certificate_id,
|
||||
variable_values: inbound.variable_values,
|
||||
is_active: inbound.is_active,
|
||||
created_at: inbound.created_at,
|
||||
updated_at: inbound.updated_at,
|
||||
template_name: None, // Will be filled by repository if needed
|
||||
certificate_name: None, // Will be filled by repository if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn apply_update(self, dto: UpdateServerInboundDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
if let Some(tag) = dto.tag {
|
||||
active_model.tag = Set(tag);
|
||||
}
|
||||
if let Some(port_override) = dto.port_override {
|
||||
active_model.port_override = Set(Some(port_override));
|
||||
}
|
||||
if let Some(certificate_id) = dto.certificate_id {
|
||||
active_model.certificate_id = Set(Some(certificate_id));
|
||||
}
|
||||
if let Some(variable_values) = dto.variable_values {
|
||||
active_model.variable_values = Set(Value::Object(variable_values));
|
||||
}
|
||||
if let Some(is_active) = dto.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
active_model
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_variable_values(&self) -> serde_json::Map<String, Value> {
|
||||
if let Value::Object(map) = &self.variable_values {
|
||||
map.clone()
|
||||
} else {
|
||||
serde_json::Map::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_effective_port(&self, template_default_port: i32) -> i32 {
|
||||
self.port_override.unwrap_or(template_default_port)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreateServerInboundDto> for ActiveModel {
|
||||
fn from(dto: CreateServerInboundDto) -> Self {
|
||||
Self {
|
||||
template_id: Set(dto.template_id),
|
||||
tag: Set(format!("inbound-{}", Uuid::new_v4())), // Generate unique tag
|
||||
port_override: Set(Some(dto.port)),
|
||||
certificate_id: Set(dto.certificate_id),
|
||||
variable_values: Set(Value::Object(serde_json::Map::new())),
|
||||
is_active: Set(dto.is_active),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/database/entities/user.rs
Normal file
185
src/database/entities/user.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
/// User display name
|
||||
pub name: String,
|
||||
|
||||
/// Optional comment/description about the user
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// Optional Telegram user ID for bot integration
|
||||
pub telegram_id: Option<i64>,
|
||||
|
||||
/// When the user was registered/created
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
/// Last time user record was updated
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
/// Called before insert and update
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Called before update
|
||||
fn before_save<'life0, 'async_trait, C>(
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !insert {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// User creation data transfer object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateUserDto {
|
||||
pub name: String,
|
||||
pub comment: Option<String>,
|
||||
pub telegram_id: Option<i64>,
|
||||
}
|
||||
|
||||
/// User update data transfer object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateUserDto {
|
||||
pub name: Option<String>,
|
||||
pub comment: Option<String>,
|
||||
pub telegram_id: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<CreateUserDto> for ActiveModel {
|
||||
fn from(dto: CreateUserDto) -> Self {
|
||||
Self {
|
||||
name: Set(dto.name),
|
||||
comment: Set(dto.comment),
|
||||
telegram_id: Set(dto.telegram_id),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Update this model with data from UpdateUserDto
|
||||
pub fn apply_update(self, dto: UpdateUserDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
if let Some(name) = dto.name {
|
||||
active_model.name = Set(name);
|
||||
}
|
||||
if let Some(comment) = dto.comment {
|
||||
active_model.comment = Set(Some(comment));
|
||||
} else if dto.comment.is_some() {
|
||||
// Explicitly set to None if Some(None) was passed
|
||||
active_model.comment = Set(None);
|
||||
}
|
||||
if dto.telegram_id.is_some() {
|
||||
active_model.telegram_id = Set(dto.telegram_id);
|
||||
}
|
||||
|
||||
active_model
|
||||
}
|
||||
|
||||
/// Check if user has Telegram integration
|
||||
#[allow(dead_code)]
|
||||
pub fn has_telegram(&self) -> bool {
|
||||
self.telegram_id.is_some()
|
||||
}
|
||||
|
||||
/// Get display name with optional comment
|
||||
#[allow(dead_code)]
|
||||
pub fn display_name(&self) -> String {
|
||||
match &self.comment {
|
||||
Some(comment) if !comment.is_empty() => format!("{} ({})", self.name, comment),
|
||||
_ => self.name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_user_dto_conversion() {
|
||||
let dto = CreateUserDto {
|
||||
name: "Test User".to_string(),
|
||||
comment: Some("Test comment".to_string()),
|
||||
telegram_id: Some(123456789),
|
||||
};
|
||||
|
||||
let active_model: ActiveModel = dto.into();
|
||||
|
||||
assert_eq!(active_model.name.unwrap(), "Test User");
|
||||
assert_eq!(active_model.comment.unwrap(), Some("Test comment".to_string()));
|
||||
assert_eq!(active_model.telegram_id.unwrap(), Some(123456789));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_display_name() {
|
||||
let user = Model {
|
||||
id: Uuid::new_v4(),
|
||||
name: "John Doe".to_string(),
|
||||
comment: Some("Admin user".to_string()),
|
||||
telegram_id: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
assert_eq!(user.display_name(), "John Doe (Admin user)");
|
||||
|
||||
let user_no_comment = Model {
|
||||
comment: None,
|
||||
..user
|
||||
};
|
||||
|
||||
assert_eq!(user_no_comment.display_name(), "John Doe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_telegram() {
|
||||
let user_with_telegram = Model {
|
||||
id: Uuid::new_v4(),
|
||||
name: "User".to_string(),
|
||||
comment: None,
|
||||
telegram_id: Some(123456789),
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let user_without_telegram = Model {
|
||||
telegram_id: None,
|
||||
..user_with_telegram.clone()
|
||||
};
|
||||
|
||||
assert!(user_with_telegram.has_telegram());
|
||||
assert!(!user_without_telegram.has_telegram());
|
||||
}
|
||||
}
|
||||
188
src/database/entities/user_access.rs
Normal file
188
src/database/entities/user_access.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user_access")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
/// User ID this access is for
|
||||
pub user_id: Uuid,
|
||||
|
||||
/// Server ID this access applies to
|
||||
pub server_id: Uuid,
|
||||
|
||||
/// Server inbound ID this access applies to
|
||||
pub server_inbound_id: Uuid,
|
||||
|
||||
/// User's unique identifier in xray (UUID for VLESS/VMess, password for Trojan)
|
||||
pub xray_user_id: String,
|
||||
|
||||
/// User's email in xray
|
||||
pub xray_email: String,
|
||||
|
||||
/// User level in xray (0-255)
|
||||
pub level: i32,
|
||||
|
||||
/// Whether this access is currently active
|
||||
pub is_active: bool,
|
||||
|
||||
/// When this access was created
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
/// Last time this access was updated
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::server::Entity",
|
||||
from = "Column::ServerId",
|
||||
to = "super::server::Column::Id"
|
||||
)]
|
||||
Server,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::server_inbound::Entity",
|
||||
from = "Column::ServerInboundId",
|
||||
to = "super::server_inbound::Column::Id"
|
||||
)]
|
||||
ServerInbound,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Server.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server_inbound::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerInbound.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn before_save<'life0, 'async_trait, C>(
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !insert {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// User access creation data transfer object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateUserAccessDto {
|
||||
pub user_id: Uuid,
|
||||
pub server_id: Uuid,
|
||||
pub server_inbound_id: Uuid,
|
||||
pub xray_user_id: String,
|
||||
pub xray_email: String,
|
||||
pub level: Option<i32>,
|
||||
}
|
||||
|
||||
/// User access update data transfer object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateUserAccessDto {
|
||||
pub is_active: Option<bool>,
|
||||
pub level: Option<i32>,
|
||||
}
|
||||
|
||||
impl From<CreateUserAccessDto> for ActiveModel {
|
||||
fn from(dto: CreateUserAccessDto) -> Self {
|
||||
Self {
|
||||
user_id: Set(dto.user_id),
|
||||
server_id: Set(dto.server_id),
|
||||
server_inbound_id: Set(dto.server_inbound_id),
|
||||
xray_user_id: Set(dto.xray_user_id),
|
||||
xray_email: Set(dto.xray_email),
|
||||
level: Set(dto.level.unwrap_or(0)),
|
||||
is_active: Set(true),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Update this model with data from UpdateUserAccessDto
|
||||
pub fn apply_update(self, dto: UpdateUserAccessDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
if let Some(is_active) = dto.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
if let Some(level) = dto.level {
|
||||
active_model.level = Set(level);
|
||||
}
|
||||
|
||||
active_model
|
||||
}
|
||||
}
|
||||
|
||||
/// Response model for user access
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserAccessResponse {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub server_id: Uuid,
|
||||
pub server_inbound_id: Uuid,
|
||||
pub xray_user_id: String,
|
||||
pub xray_email: String,
|
||||
pub level: i32,
|
||||
pub is_active: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<Model> for UserAccessResponse {
|
||||
fn from(model: Model) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
user_id: model.user_id,
|
||||
server_id: model.server_id,
|
||||
server_inbound_id: model.server_inbound_id,
|
||||
xray_user_id: model.xray_user_id,
|
||||
xray_email: model.xray_email,
|
||||
level: model.level,
|
||||
is_active: model.is_active,
|
||||
created_at: model.created_at.to_rfc3339(),
|
||||
updated_at: model.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user