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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/database/migrations/m20241201_000001_create_users_table.rs
Normal file
135
src/database/migrations/m20241201_000001_create_users_table.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// Create users table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Users::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Users::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::Name)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::Comment)
|
||||
.text()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::TelegramId)
|
||||
.big_integer()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create index on name for faster searches
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_users_name")
|
||||
.table(Users::Table)
|
||||
.col(Users::Name)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create unique index on telegram_id (if not null)
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_users_telegram_id")
|
||||
.table(Users::Table)
|
||||
.col(Users::TelegramId)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create index on created_at for sorting
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_users_created_at")
|
||||
.table(Users::Table)
|
||||
.col(Users::CreatedAt)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// Drop indexes first
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_users_created_at")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_users_telegram_id")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_users_name")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Drop table
|
||||
manager
|
||||
.drop_table(Table::drop().table(Users::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Comment,
|
||||
TelegramId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Certificates::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Certificates::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::Name)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::CertType)
|
||||
.string_len(50)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::Domain)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::CertData)
|
||||
.blob()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::KeyData)
|
||||
.blob()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::ChainData)
|
||||
.blob()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::ExpiresAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::AutoRenew)
|
||||
.boolean()
|
||||
.default(false)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Certificates::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Index on domain for faster lookups
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_certificates_domain")
|
||||
.table(Certificates::Table)
|
||||
.col(Certificates::Domain)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_certificates_domain")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_table(Table::drop().table(Certificates::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Certificates {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
CertType,
|
||||
Domain,
|
||||
CertData,
|
||||
KeyData,
|
||||
ChainData,
|
||||
ExpiresAt,
|
||||
AutoRenew,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(InboundTemplates::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::Name)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::Description)
|
||||
.text()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::Protocol)
|
||||
.string_len(50)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::DefaultPort)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::BaseSettings)
|
||||
.json()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::StreamSettings)
|
||||
.json()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::RequiresTls)
|
||||
.boolean()
|
||||
.default(false)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::RequiresDomain)
|
||||
.boolean()
|
||||
.default(false)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::Variables)
|
||||
.json()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::IsActive)
|
||||
.boolean()
|
||||
.default(true)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundTemplates::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Index on name for searches
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_inbound_templates_name")
|
||||
.table(InboundTemplates::Table)
|
||||
.col(InboundTemplates::Name)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Index on protocol
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_inbound_templates_protocol")
|
||||
.table(InboundTemplates::Table)
|
||||
.col(InboundTemplates::Protocol)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_inbound_templates_protocol")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_inbound_templates_name")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_table(Table::drop().table(InboundTemplates::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum InboundTemplates {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Description,
|
||||
Protocol,
|
||||
DefaultPort,
|
||||
BaseSettings,
|
||||
StreamSettings,
|
||||
RequiresTls,
|
||||
RequiresDomain,
|
||||
Variables,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
136
src/database/migrations/m20241201_000004_create_servers_table.rs
Normal file
136
src/database/migrations/m20241201_000004_create_servers_table.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Servers::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Servers::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Servers::Name)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Servers::Hostname)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Servers::GrpcPort)
|
||||
.integer()
|
||||
.default(2053)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Servers::ApiCredentials)
|
||||
.text()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Servers::Status)
|
||||
.string_len(50)
|
||||
.default("unknown")
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Servers::DefaultCertificateId)
|
||||
.uuid()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Servers::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Servers::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Foreign key to certificates
|
||||
manager
|
||||
.create_foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_servers_default_certificate")
|
||||
.from(Servers::Table, Servers::DefaultCertificateId)
|
||||
.to(Certificates::Table, Certificates::Id)
|
||||
.on_delete(ForeignKeyAction::SetNull)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Index on hostname
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_servers_hostname")
|
||||
.table(Servers::Table)
|
||||
.col(Servers::Hostname)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_foreign_key(
|
||||
ForeignKey::drop()
|
||||
.name("fk_servers_default_certificate")
|
||||
.table(Servers::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_servers_hostname")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_table(Table::drop().table(Servers::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Servers {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Hostname,
|
||||
GrpcPort,
|
||||
ApiCredentials,
|
||||
Status,
|
||||
DefaultCertificateId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Certificates {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ServerInbounds::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::ServerId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::TemplateId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::Tag)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::PortOverride)
|
||||
.integer()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::CertificateId)
|
||||
.uuid()
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::VariableValues)
|
||||
.json()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::IsActive)
|
||||
.boolean()
|
||||
.default(true)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ServerInbounds::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Foreign keys
|
||||
manager
|
||||
.create_foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_server_inbounds_server")
|
||||
.from(ServerInbounds::Table, ServerInbounds::ServerId)
|
||||
.to(Servers::Table, Servers::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_server_inbounds_template")
|
||||
.from(ServerInbounds::Table, ServerInbounds::TemplateId)
|
||||
.to(InboundTemplates::Table, InboundTemplates::Id)
|
||||
.on_delete(ForeignKeyAction::Restrict)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_server_inbounds_certificate")
|
||||
.from(ServerInbounds::Table, ServerInbounds::CertificateId)
|
||||
.to(Certificates::Table, Certificates::Id)
|
||||
.on_delete(ForeignKeyAction::SetNull)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Unique constraint on server_id + tag
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_server_inbounds_server_tag")
|
||||
.table(ServerInbounds::Table)
|
||||
.col(ServerInbounds::ServerId)
|
||||
.col(ServerInbounds::Tag)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_foreign_key(
|
||||
ForeignKey::drop()
|
||||
.name("fk_server_inbounds_certificate")
|
||||
.table(ServerInbounds::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_foreign_key(
|
||||
ForeignKey::drop()
|
||||
.name("fk_server_inbounds_template")
|
||||
.table(ServerInbounds::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_foreign_key(
|
||||
ForeignKey::drop()
|
||||
.name("fk_server_inbounds_server")
|
||||
.table(ServerInbounds::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_server_inbounds_server_tag")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_table(Table::drop().table(ServerInbounds::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ServerInbounds {
|
||||
Table,
|
||||
Id,
|
||||
ServerId,
|
||||
TemplateId,
|
||||
Tag,
|
||||
PortOverride,
|
||||
CertificateId,
|
||||
VariableValues,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Servers {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum InboundTemplates {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Certificates {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(UserAccess::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::UserId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::ServerId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::ServerInboundId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::XrayUserId)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::XrayEmail)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::Level)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::IsActive)
|
||||
.boolean()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserAccess::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_user_access_user_id")
|
||||
.from(UserAccess::Table, UserAccess::UserId)
|
||||
.to(Users::Table, Users::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_user_access_server_id")
|
||||
.from(UserAccess::Table, UserAccess::ServerId)
|
||||
.to(Servers::Table, Servers::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_user_access_server_inbound_id")
|
||||
.from(UserAccess::Table, UserAccess::ServerInboundId)
|
||||
.to(ServerInbounds::Table, ServerInbounds::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create indexes separately
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_user_access_server_inbound")
|
||||
.table(UserAccess::Table)
|
||||
.col(UserAccess::ServerId)
|
||||
.col(UserAccess::ServerInboundId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_user_access_user_server")
|
||||
.table(UserAccess::Table)
|
||||
.col(UserAccess::UserId)
|
||||
.col(UserAccess::ServerId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_user_access_xray_email")
|
||||
.table(UserAccess::Table)
|
||||
.col(UserAccess::XrayEmail)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// Drop indexes first
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_user_access_xray_email")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_user_access_user_server")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_user_access_server_inbound")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Drop table
|
||||
manager
|
||||
.drop_table(Table::drop().table(UserAccess::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum UserAccess {
|
||||
Table,
|
||||
Id,
|
||||
UserId,
|
||||
ServerId,
|
||||
ServerInboundId,
|
||||
XrayUserId,
|
||||
XrayEmail,
|
||||
Level,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Servers {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ServerInbounds {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(InboundUsers::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::ServerInboundId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::Username)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::Email)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::XrayUserId)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::Level)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::IsActive)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(InboundUsers::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_inbound_users_server_inbound")
|
||||
.from(InboundUsers::Table, InboundUsers::ServerInboundId)
|
||||
.to(ServerInbounds::Table, ServerInbounds::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create unique constraint: one user per inbound
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_inbound_users_unique_user_per_inbound")
|
||||
.table(InboundUsers::Table)
|
||||
.col(InboundUsers::ServerInboundId)
|
||||
.col(InboundUsers::Username)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create index on email for faster lookups
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_inbound_users_email")
|
||||
.table(InboundUsers::Table)
|
||||
.col(InboundUsers::Email)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(InboundUsers::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum InboundUsers {
|
||||
Table,
|
||||
Id,
|
||||
ServerInboundId,
|
||||
Username,
|
||||
Email,
|
||||
XrayUserId,
|
||||
Level,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ServerInbounds {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
26
src/database/migrations/mod.rs
Normal file
26
src/database/migrations/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20241201_000001_create_users_table;
|
||||
mod m20241201_000002_create_certificates_table;
|
||||
mod m20241201_000003_create_inbound_templates_table;
|
||||
mod m20241201_000004_create_servers_table;
|
||||
mod m20241201_000005_create_server_inbounds_table;
|
||||
mod m20241201_000006_create_user_access_table;
|
||||
mod m20241201_000007_create_inbound_users_table;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20241201_000001_create_users_table::Migration),
|
||||
Box::new(m20241201_000002_create_certificates_table::Migration),
|
||||
Box::new(m20241201_000003_create_inbound_templates_table::Migration),
|
||||
Box::new(m20241201_000004_create_servers_table::Migration),
|
||||
Box::new(m20241201_000005_create_server_inbounds_table::Migration),
|
||||
Box::new(m20241201_000006_create_user_access_table::Migration),
|
||||
Box::new(m20241201_000007_create_inbound_users_table::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
161
src/database/mod.rs
Normal file
161
src/database/mod.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{Database, DatabaseConnection, ConnectOptions, Statement, DatabaseBackend, ConnectionTrait};
|
||||
use sea_orm_migration::MigratorTrait;
|
||||
use std::time::Duration;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::config::DatabaseConfig;
|
||||
|
||||
pub mod entities;
|
||||
pub mod migrations;
|
||||
pub mod repository;
|
||||
|
||||
use migrations::Migrator;
|
||||
|
||||
/// Database connection and management
|
||||
#[derive(Clone)]
|
||||
pub struct DatabaseManager {
|
||||
connection: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl DatabaseManager {
|
||||
/// Create a new database connection
|
||||
pub async fn new(config: &DatabaseConfig) -> Result<Self> {
|
||||
info!("Connecting to database...");
|
||||
|
||||
// URL-encode the connection string to handle special characters in passwords
|
||||
let encoded_url = Self::encode_database_url(&config.url)?;
|
||||
|
||||
let mut opt = ConnectOptions::new(&encoded_url);
|
||||
opt.max_connections(config.max_connections)
|
||||
.min_connections(1)
|
||||
.connect_timeout(Duration::from_secs(config.connection_timeout))
|
||||
.acquire_timeout(Duration::from_secs(config.connection_timeout))
|
||||
.idle_timeout(Duration::from_secs(600))
|
||||
.max_lifetime(Duration::from_secs(3600))
|
||||
.sqlx_logging(tracing::level_enabled!(tracing::Level::DEBUG))
|
||||
.sqlx_logging_level(log::LevelFilter::Debug);
|
||||
|
||||
let connection = Database::connect(opt).await?;
|
||||
|
||||
info!("Database connection established successfully");
|
||||
|
||||
let manager = Self { connection };
|
||||
|
||||
// Run migrations if auto_migrate is enabled
|
||||
if config.auto_migrate {
|
||||
manager.migrate().await?;
|
||||
}
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
/// Get database connection
|
||||
pub fn connection(&self) -> &DatabaseConnection {
|
||||
&self.connection
|
||||
}
|
||||
|
||||
/// Run database migrations
|
||||
pub async fn migrate(&self) -> Result<()> {
|
||||
info!("Running database migrations...");
|
||||
|
||||
match Migrator::up(&self.connection, None).await {
|
||||
Ok(_) => {
|
||||
info!("Database migrations completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Migration error: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check database connection health
|
||||
pub async fn health_check(&self) -> Result<bool> {
|
||||
let stmt = Statement::from_string(DatabaseBackend::Postgres, "SELECT 1".to_owned());
|
||||
match self.connection.execute(stmt).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => {
|
||||
warn!("Database health check failed: {}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get database schema information
|
||||
pub async fn get_schema_version(&self) -> Result<Option<String>> {
|
||||
// This would typically query a migrations table
|
||||
// For now, we'll just return a placeholder
|
||||
Ok(Some("1.0.0".to_string()))
|
||||
}
|
||||
|
||||
/// Encode database URL to handle special characters in passwords
|
||||
fn encode_database_url(url: &str) -> Result<String> {
|
||||
// Parse URL manually to handle special characters in password
|
||||
if let Some(at_pos) = url.rfind('@') {
|
||||
if let Some(_colon_pos) = url[..at_pos].rfind(':') {
|
||||
if let Some(scheme_end) = url.find("://") {
|
||||
let scheme = &url[..scheme_end + 3];
|
||||
let user_pass = &url[scheme_end + 3..at_pos];
|
||||
let host_db = &url[at_pos..];
|
||||
|
||||
if let Some(user_colon) = user_pass.find(':') {
|
||||
let user = &user_pass[..user_colon];
|
||||
let password = &user_pass[user_colon + 1..];
|
||||
|
||||
// URL-encode the password part only
|
||||
let encoded_password = urlencoding::encode(password);
|
||||
let encoded_url = format!("{}{}:{}{}", scheme, user, encoded_password, host_db);
|
||||
|
||||
return Ok(encoded_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If parsing fails, return original URL
|
||||
Ok(url.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::DatabaseConfig;
|
||||
|
||||
#[test]
|
||||
fn test_encode_database_url() {
|
||||
let url_with_special_chars = "postgresql://user:pass#word@localhost:5432/db";
|
||||
let encoded = DatabaseManager::encode_database_url(url_with_special_chars).unwrap();
|
||||
assert_eq!(encoded, "postgresql://user:pass%23word@localhost:5432/db");
|
||||
|
||||
let normal_url = "postgresql://user:password@localhost:5432/db";
|
||||
let encoded_normal = DatabaseManager::encode_database_url(normal_url).unwrap();
|
||||
assert_eq!(encoded_normal, "postgresql://user:password@localhost:5432/db");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_database_connection() {
|
||||
// This test requires a running PostgreSQL database
|
||||
// Skip in CI or when database is not available
|
||||
if std::env::var("DATABASE_URL").is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let config = DatabaseConfig {
|
||||
url: std::env::var("DATABASE_URL").unwrap(),
|
||||
max_connections: 5,
|
||||
connection_timeout: 30,
|
||||
auto_migrate: false,
|
||||
};
|
||||
|
||||
let db = DatabaseManager::new(&config).await;
|
||||
assert!(db.is_ok());
|
||||
|
||||
if let Ok(db) = db {
|
||||
let health = db.health_check().await;
|
||||
assert!(health.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/database/repository/certificate.rs
Normal file
75
src/database/repository/certificate.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use sea_orm::*;
|
||||
use crate::database::entities::{certificate, prelude::*};
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CertificateRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl CertificateRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn create(&self, cert_data: certificate::CreateCertificateDto) -> Result<certificate::Model> {
|
||||
let cert = certificate::ActiveModel::from(cert_data);
|
||||
|
||||
let result = Certificate::insert(cert).exec(&self.db).await?;
|
||||
|
||||
Certificate::find_by_id(result.last_insert_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created certificate"))
|
||||
}
|
||||
|
||||
pub async fn find_all(&self) -> Result<Vec<certificate::Model>> {
|
||||
Ok(Certificate::find().all(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<certificate::Model>> {
|
||||
Ok(Certificate::find_by_id(id).one(&self.db).await?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn find_by_domain(&self, domain: &str) -> Result<Vec<certificate::Model>> {
|
||||
Ok(Certificate::find()
|
||||
.filter(certificate::Column::Domain.eq(domain))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn find_by_type(&self, cert_type: &str) -> Result<Vec<certificate::Model>> {
|
||||
Ok(Certificate::find()
|
||||
.filter(certificate::Column::CertType.eq(cert_type))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, cert_data: certificate::UpdateCertificateDto) -> Result<certificate::Model> {
|
||||
let cert = Certificate::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Certificate not found"))?;
|
||||
|
||||
let updated_cert = cert.apply_update(cert_data);
|
||||
|
||||
Ok(updated_cert.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool> {
|
||||
let result = Certificate::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
pub async fn find_expiring_soon(&self, days: i64) -> Result<Vec<certificate::Model>> {
|
||||
let threshold = chrono::Utc::now() + chrono::Duration::days(days);
|
||||
|
||||
Ok(Certificate::find()
|
||||
.filter(certificate::Column::ExpiresAt.lt(threshold))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
65
src/database/repository/inbound_template.rs
Normal file
65
src/database/repository/inbound_template.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use sea_orm::*;
|
||||
use crate::database::entities::{inbound_template, prelude::*};
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InboundTemplateRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl InboundTemplateRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn create(&self, template_data: inbound_template::CreateInboundTemplateDto) -> Result<inbound_template::Model> {
|
||||
let template = inbound_template::ActiveModel::from(template_data);
|
||||
|
||||
let result = InboundTemplate::insert(template).exec(&self.db).await?;
|
||||
|
||||
InboundTemplate::find_by_id(result.last_insert_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created template"))
|
||||
}
|
||||
|
||||
pub async fn find_all(&self) -> Result<Vec<inbound_template::Model>> {
|
||||
Ok(InboundTemplate::find().all(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<inbound_template::Model>> {
|
||||
Ok(InboundTemplate::find_by_id(id).one(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_name(&self, name: &str) -> Result<Option<inbound_template::Model>> {
|
||||
Ok(InboundTemplate::find()
|
||||
.filter(inbound_template::Column::Name.eq(name))
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_protocol(&self, protocol: &str) -> Result<Vec<inbound_template::Model>> {
|
||||
Ok(InboundTemplate::find()
|
||||
.filter(inbound_template::Column::Protocol.eq(protocol))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, template_data: inbound_template::UpdateInboundTemplateDto) -> Result<inbound_template::Model> {
|
||||
let template = InboundTemplate::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Template not found"))?;
|
||||
|
||||
let updated_template = template.apply_update(template_data);
|
||||
|
||||
Ok(updated_template.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool> {
|
||||
let result = InboundTemplate::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
132
src/database/repository/inbound_users.rs
Normal file
132
src/database/repository/inbound_users.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter, Set};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::inbound_users::{
|
||||
Entity, Model, ActiveModel, CreateInboundUserDto, UpdateInboundUserDto, Column
|
||||
};
|
||||
|
||||
pub struct InboundUsersRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl InboundUsersRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(&self) -> Result<Vec<Model>> {
|
||||
let users = Entity::find().all(&self.db).await?;
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let user = Entity::find_by_id(id).one(&self.db).await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Find all users for a specific inbound
|
||||
pub async fn find_by_inbound_id(&self, inbound_id: Uuid) -> Result<Vec<Model>> {
|
||||
let users = Entity::find()
|
||||
.filter(Column::ServerInboundId.eq(inbound_id))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
/// Find active users for a specific inbound
|
||||
pub async fn find_active_by_inbound_id(&self, inbound_id: Uuid) -> Result<Vec<Model>> {
|
||||
let users = Entity::find()
|
||||
.filter(Column::ServerInboundId.eq(inbound_id))
|
||||
.filter(Column::IsActive.eq(true))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
/// Find user by username and inbound (for uniqueness check)
|
||||
pub async fn find_by_username_and_inbound(&self, username: &str, inbound_id: Uuid) -> Result<Option<Model>> {
|
||||
let user = Entity::find()
|
||||
.filter(Column::Username.eq(username))
|
||||
.filter(Column::ServerInboundId.eq(inbound_id))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Find user by email
|
||||
pub async fn find_by_email(&self, email: &str) -> Result<Option<Model>> {
|
||||
let user = Entity::find()
|
||||
.filter(Column::Email.eq(email))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn create(&self, dto: CreateInboundUserDto) -> Result<Model> {
|
||||
let active_model: ActiveModel = dto.into();
|
||||
let user = active_model.insert(&self.db).await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, dto: UpdateInboundUserDto) -> Result<Option<Model>> {
|
||||
let user = match self.find_by_id(id).await? {
|
||||
Some(user) => user,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let updated_model = user.apply_update(dto);
|
||||
let updated_user = updated_model.update(&self.db).await?;
|
||||
Ok(Some(updated_user))
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool> {
|
||||
let result = Entity::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
/// Enable user (set is_active = true)
|
||||
pub async fn enable(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let user = match self.find_by_id(id).await? {
|
||||
Some(user) => user,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let mut active_model: ActiveModel = user.into();
|
||||
active_model.is_active = Set(true);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
let updated_user = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated_user))
|
||||
}
|
||||
|
||||
/// Disable user (set is_active = false)
|
||||
pub async fn disable(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let user = match self.find_by_id(id).await? {
|
||||
Some(user) => user,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let mut active_model: ActiveModel = user.into();
|
||||
active_model.is_active = Set(false);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
let updated_user = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated_user))
|
||||
}
|
||||
|
||||
/// Remove all users for a specific inbound (when inbound is deleted)
|
||||
pub async fn remove_all_for_inbound(&self, inbound_id: Uuid) -> Result<u64> {
|
||||
let result = Entity::delete_many()
|
||||
.filter(Column::ServerInboundId.eq(inbound_id))
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected)
|
||||
}
|
||||
|
||||
/// Check if username already exists on this inbound
|
||||
pub async fn username_exists_on_inbound(&self, username: &str, inbound_id: Uuid) -> Result<bool> {
|
||||
let exists = self.find_by_username_and_inbound(username, inbound_id).await?;
|
||||
Ok(exists.is_some())
|
||||
}
|
||||
}
|
||||
15
src/database/repository/mod.rs
Normal file
15
src/database/repository/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
pub mod user;
|
||||
pub mod certificate;
|
||||
pub mod inbound_template;
|
||||
pub mod server;
|
||||
pub mod server_inbound;
|
||||
pub mod user_access;
|
||||
pub mod inbound_users;
|
||||
|
||||
pub use user::UserRepository;
|
||||
pub use certificate::CertificateRepository;
|
||||
pub use inbound_template::InboundTemplateRepository;
|
||||
pub use server::ServerRepository;
|
||||
pub use server_inbound::ServerInboundRepository;
|
||||
pub use user_access::UserAccessRepository;
|
||||
pub use inbound_users::InboundUsersRepository;
|
||||
79
src/database/repository/server.rs
Normal file
79
src/database/repository/server.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use sea_orm::*;
|
||||
use crate::database::entities::{server, prelude::*};
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl ServerRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn create(&self, server_data: server::CreateServerDto) -> Result<server::Model> {
|
||||
let server = server::ActiveModel::from(server_data);
|
||||
|
||||
let result = Server::insert(server).exec(&self.db).await?;
|
||||
|
||||
Server::find_by_id(result.last_insert_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created server"))
|
||||
}
|
||||
|
||||
pub async fn find_all(&self) -> Result<Vec<server::Model>> {
|
||||
Ok(Server::find().all(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<server::Model>> {
|
||||
Ok(Server::find_by_id(id).one(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_name(&self, name: &str) -> Result<Option<server::Model>> {
|
||||
Ok(Server::find()
|
||||
.filter(server::Column::Name.eq(name))
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_hostname(&self, hostname: &str) -> Result<Option<server::Model>> {
|
||||
Ok(Server::find()
|
||||
.filter(server::Column::Hostname.eq(hostname))
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_status(&self, status: &str) -> Result<Vec<server::Model>> {
|
||||
Ok(Server::find()
|
||||
.filter(server::Column::Status.eq(status))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, server_data: server::UpdateServerDto) -> Result<server::Model> {
|
||||
let server = Server::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server not found"))?;
|
||||
|
||||
let updated_server = server.apply_update(server_data);
|
||||
|
||||
Ok(updated_server.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool> {
|
||||
let result = Server::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
pub async fn get_grpc_endpoint(&self, id: Uuid) -> Result<String> {
|
||||
let server = self.find_by_id(id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server not found"))?;
|
||||
|
||||
Ok(format!("{}:{}", server.hostname, server.grpc_port))
|
||||
}
|
||||
}
|
||||
159
src/database/repository/server_inbound.rs
Normal file
159
src/database/repository/server_inbound.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use sea_orm::*;
|
||||
use crate::database::entities::{server_inbound, prelude::*};
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerInboundRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl ServerInboundRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn create(&self, server_id: Uuid, inbound_data: server_inbound::CreateServerInboundDto) -> Result<server_inbound::Model> {
|
||||
let mut inbound: server_inbound::ActiveModel = inbound_data.into();
|
||||
inbound.id = Set(Uuid::new_v4());
|
||||
inbound.server_id = Set(server_id);
|
||||
inbound.created_at = Set(chrono::Utc::now());
|
||||
inbound.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
let result = ServerInbound::insert(inbound).exec(&self.db).await?;
|
||||
|
||||
ServerInbound::find_by_id(result.last_insert_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created server inbound"))
|
||||
}
|
||||
|
||||
pub async fn create_with_protocol(&self, server_id: Uuid, inbound_data: server_inbound::CreateServerInboundDto, protocol: &str) -> Result<server_inbound::Model> {
|
||||
let mut inbound: server_inbound::ActiveModel = inbound_data.into();
|
||||
inbound.id = Set(Uuid::new_v4());
|
||||
inbound.server_id = Set(server_id);
|
||||
inbound.created_at = Set(chrono::Utc::now());
|
||||
inbound.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
// Override tag with protocol prefix
|
||||
let id = inbound.id.as_ref();
|
||||
inbound.tag = Set(format!("{}-inbound-{}", protocol, id));
|
||||
|
||||
let result = ServerInbound::insert(inbound).exec(&self.db).await?;
|
||||
|
||||
ServerInbound::find_by_id(result.last_insert_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to retrieve created server inbound"))
|
||||
}
|
||||
|
||||
pub async fn find_all(&self) -> Result<Vec<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find().all(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find_by_id(id).one(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_server_id(&self, server_id: Uuid) -> Result<Vec<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find()
|
||||
.filter(server_inbound::Column::ServerId.eq(server_id))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_server_id_with_template(&self, server_id: Uuid) -> Result<Vec<server_inbound::ServerInboundResponse>> {
|
||||
use crate::database::entities::{inbound_template, certificate};
|
||||
|
||||
let inbounds = ServerInbound::find()
|
||||
.filter(server_inbound::Column::ServerId.eq(server_id))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
let mut responses = Vec::new();
|
||||
for inbound in inbounds {
|
||||
let mut response = server_inbound::ServerInboundResponse::from(inbound.clone());
|
||||
|
||||
// Load template information
|
||||
if let Ok(Some(template)) = InboundTemplate::find_by_id(inbound.template_id).one(&self.db).await {
|
||||
response.template_name = Some(template.name);
|
||||
}
|
||||
|
||||
// Load certificate information
|
||||
if let Some(cert_id) = inbound.certificate_id {
|
||||
if let Ok(Some(certificate)) = Certificate::find_by_id(cert_id).one(&self.db).await {
|
||||
response.certificate_name = Some(certificate.domain);
|
||||
}
|
||||
}
|
||||
|
||||
responses.push(response);
|
||||
}
|
||||
|
||||
Ok(responses)
|
||||
}
|
||||
|
||||
pub async fn find_by_template_id(&self, template_id: Uuid) -> Result<Vec<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find()
|
||||
.filter(server_inbound::Column::TemplateId.eq(template_id))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_tag(&self, tag: &str) -> Result<Option<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find()
|
||||
.filter(server_inbound::Column::Tag.eq(tag))
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_active_by_server(&self, server_id: Uuid) -> Result<Vec<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find()
|
||||
.filter(server_inbound::Column::ServerId.eq(server_id))
|
||||
.filter(server_inbound::Column::IsActive.eq(true))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, inbound_data: server_inbound::UpdateServerInboundDto) -> Result<server_inbound::Model> {
|
||||
let inbound = ServerInbound::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
|
||||
|
||||
let updated_inbound = inbound.apply_update(inbound_data);
|
||||
|
||||
Ok(updated_inbound.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool> {
|
||||
let result = ServerInbound::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
pub async fn activate(&self, id: Uuid) -> Result<server_inbound::Model> {
|
||||
let inbound = ServerInbound::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
|
||||
|
||||
let mut inbound: server_inbound::ActiveModel = inbound.into();
|
||||
inbound.is_active = Set(true);
|
||||
inbound.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(inbound.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn deactivate(&self, id: Uuid) -> Result<server_inbound::Model> {
|
||||
let inbound = ServerInbound::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
|
||||
|
||||
let mut inbound: server_inbound::ActiveModel = inbound.into();
|
||||
inbound.is_active = Set(false);
|
||||
inbound.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(inbound.update(&self.db).await?)
|
||||
}
|
||||
}
|
||||
157
src/database/repository/user.rs
Normal file
157
src/database/repository/user.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::user::{Entity as User, Column, Model, ActiveModel, CreateUserDto, UpdateUserDto};
|
||||
|
||||
pub struct UserRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl UserRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
/// Get all users with pagination
|
||||
pub async fn get_all(&self, page: u64, per_page: u64) -> Result<Vec<Model>> {
|
||||
let users = User::find()
|
||||
.order_by_desc(Column::CreatedAt)
|
||||
.paginate(&self.db, per_page)
|
||||
.fetch_page(page.saturating_sub(1))
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
/// Get user by ID
|
||||
pub async fn get_by_id(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let user = User::find_by_id(id).one(&self.db).await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Get user by telegram ID
|
||||
pub async fn get_by_telegram_id(&self, telegram_id: i64) -> Result<Option<Model>> {
|
||||
let user = User::find()
|
||||
.filter(Column::TelegramId.eq(telegram_id))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Search users by name
|
||||
pub async fn search_by_name(&self, query: &str, page: u64, per_page: u64) -> Result<Vec<Model>> {
|
||||
let users = User::find()
|
||||
.filter(Column::Name.contains(query))
|
||||
.order_by_desc(Column::CreatedAt)
|
||||
.paginate(&self.db, per_page)
|
||||
.fetch_page(page.saturating_sub(1))
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
/// Create a new user
|
||||
pub async fn create(&self, dto: CreateUserDto) -> Result<Model> {
|
||||
let active_model: ActiveModel = dto.into();
|
||||
let user = User::insert(active_model).exec_with_returning(&self.db).await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Update user by ID
|
||||
pub async fn update(&self, id: Uuid, dto: UpdateUserDto) -> Result<Option<Model>> {
|
||||
if let Some(user) = self.get_by_id(id).await? {
|
||||
let active_model = user.apply_update(dto);
|
||||
User::update(active_model).exec(&self.db).await?;
|
||||
// Fetch the updated user
|
||||
self.get_by_id(id).await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete user by ID
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool> {
|
||||
let result = User::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
/// Get total count of users
|
||||
pub async fn count(&self) -> Result<u64> {
|
||||
let count = User::find().count(&self.db).await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Check if telegram ID is already used
|
||||
pub async fn telegram_id_exists(&self, telegram_id: i64) -> Result<bool> {
|
||||
let count = User::find()
|
||||
.filter(Column::TelegramId.eq(telegram_id))
|
||||
.count(&self.db)
|
||||
.await?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::config::DatabaseConfig;
|
||||
|
||||
async fn setup_test_db() -> Result<UserRepository> {
|
||||
let config = DatabaseConfig {
|
||||
url: std::env::var("DATABASE_URL").unwrap_or_else(|_|
|
||||
"sqlite::memory:".to_string()
|
||||
),
|
||||
max_connections: 5,
|
||||
connection_timeout: 30,
|
||||
auto_migrate: true,
|
||||
};
|
||||
|
||||
let db_manager = DatabaseManager::new(&config).await?;
|
||||
Ok(UserRepository::new(db_manager.connection().clone()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_crud() {
|
||||
let repo = match setup_test_db().await {
|
||||
Ok(repo) => repo,
|
||||
Err(_) => return, // Skip test if no database available
|
||||
};
|
||||
|
||||
// Create user
|
||||
let create_dto = CreateUserDto {
|
||||
name: "Test User".to_string(),
|
||||
comment: Some("Test comment".to_string()),
|
||||
telegram_id: Some(123456789),
|
||||
};
|
||||
|
||||
let created_user = repo.create(create_dto).await.unwrap();
|
||||
assert_eq!(created_user.name, "Test User");
|
||||
assert_eq!(created_user.telegram_id, Some(123456789));
|
||||
|
||||
// Get by ID
|
||||
let fetched_user = repo.get_by_id(created_user.id).await.unwrap();
|
||||
assert!(fetched_user.is_some());
|
||||
assert_eq!(fetched_user.unwrap().name, "Test User");
|
||||
|
||||
// Update user
|
||||
let update_dto = UpdateUserDto {
|
||||
name: Some("Updated User".to_string()),
|
||||
comment: None,
|
||||
telegram_id: None,
|
||||
};
|
||||
|
||||
let updated_user = repo.update(created_user.id, update_dto).await.unwrap();
|
||||
assert!(updated_user.is_some());
|
||||
assert_eq!(updated_user.unwrap().name, "Updated User");
|
||||
|
||||
// Delete user
|
||||
let deleted = repo.delete(created_user.id).await.unwrap();
|
||||
assert!(deleted);
|
||||
|
||||
// Verify deletion
|
||||
let deleted_user = repo.get_by_id(created_user.id).await.unwrap();
|
||||
assert!(deleted_user.is_none());
|
||||
}
|
||||
}
|
||||
118
src/database/repository/user_access.rs
Normal file
118
src/database/repository/user_access.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::database::entities::user_access::{self, Entity as UserAccess, Model, ActiveModel, CreateUserAccessDto, UpdateUserAccessDto};
|
||||
|
||||
pub struct UserAccessRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl UserAccessRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
/// Find all user access records
|
||||
pub async fn find_all(&self) -> Result<Vec<Model>> {
|
||||
let records = UserAccess::find().all(&self.db).await?;
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Find user access by ID
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let record = UserAccess::find_by_id(id).one(&self.db).await?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// Find user access by user ID
|
||||
pub async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<Model>> {
|
||||
let records = UserAccess::find()
|
||||
.filter(user_access::Column::UserId.eq(user_id))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Find user access by server and inbound
|
||||
pub async fn find_by_server_inbound(&self, server_id: Uuid, server_inbound_id: Uuid) -> Result<Vec<Model>> {
|
||||
let records = UserAccess::find()
|
||||
.filter(user_access::Column::ServerId.eq(server_id))
|
||||
.filter(user_access::Column::ServerInboundId.eq(server_inbound_id))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Find active user access for specific user, server and inbound
|
||||
pub async fn find_active_access(&self, user_id: Uuid, server_id: Uuid, server_inbound_id: Uuid) -> Result<Option<Model>> {
|
||||
let record = UserAccess::find()
|
||||
.filter(user_access::Column::UserId.eq(user_id))
|
||||
.filter(user_access::Column::ServerId.eq(server_id))
|
||||
.filter(user_access::Column::ServerInboundId.eq(server_inbound_id))
|
||||
.filter(user_access::Column::IsActive.eq(true))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// Create new user access
|
||||
pub async fn create(&self, dto: CreateUserAccessDto) -> Result<Model> {
|
||||
let active_model: ActiveModel = dto.into();
|
||||
let model = active_model.insert(&self.db).await?;
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
/// Update user access
|
||||
pub async fn update(&self, id: Uuid, dto: UpdateUserAccessDto) -> Result<Option<Model>> {
|
||||
let existing = match self.find_by_id(id).await? {
|
||||
Some(model) => model,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let active_model = existing.apply_update(dto);
|
||||
let updated = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated))
|
||||
}
|
||||
|
||||
/// Delete user access
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool> {
|
||||
let result = UserAccess::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
/// Enable user access (set is_active = true)
|
||||
pub async fn enable(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
self.update(id, UpdateUserAccessDto {
|
||||
is_active: Some(true),
|
||||
level: None,
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Disable user access (set is_active = false)
|
||||
pub async fn disable(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
self.update(id, UpdateUserAccessDto {
|
||||
is_active: Some(false),
|
||||
level: None,
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Get all active access for a user
|
||||
pub async fn find_active_for_user(&self, user_id: Uuid) -> Result<Vec<Model>> {
|
||||
let records = UserAccess::find()
|
||||
.filter(user_access::Column::UserId.eq(user_id))
|
||||
.filter(user_access::Column::IsActive.eq(true))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Remove all access for a specific server inbound
|
||||
pub async fn remove_all_for_inbound(&self, server_inbound_id: Uuid) -> Result<u64> {
|
||||
let result = UserAccess::delete_many()
|
||||
.filter(user_access::Column::ServerInboundId.eq(server_inbound_id))
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user