mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-25 09:49:08 +00:00
Letsencrypt works
This commit is contained in:
@@ -86,6 +86,7 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
pub enum CertificateType {
|
||||
SelfSigned,
|
||||
Imported,
|
||||
LetsEncrypt,
|
||||
}
|
||||
|
||||
impl From<CertificateType> for String {
|
||||
@@ -93,6 +94,7 @@ impl From<CertificateType> for String {
|
||||
match cert_type {
|
||||
CertificateType::SelfSigned => "self_signed".to_string(),
|
||||
CertificateType::Imported => "imported".to_string(),
|
||||
CertificateType::LetsEncrypt => "letsencrypt".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +104,7 @@ impl From<String> for CertificateType {
|
||||
match s.as_str() {
|
||||
"self_signed" => CertificateType::SelfSigned,
|
||||
"imported" => CertificateType::Imported,
|
||||
"letsencrypt" => CertificateType::LetsEncrypt,
|
||||
_ => CertificateType::SelfSigned,
|
||||
}
|
||||
}
|
||||
@@ -117,6 +120,9 @@ pub struct CreateCertificateDto {
|
||||
pub certificate_pem: String,
|
||||
#[serde(default)]
|
||||
pub private_key: String,
|
||||
// For Let's Encrypt certificates via DNS challenge
|
||||
pub dns_provider_id: Option<Uuid>,
|
||||
pub acme_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
156
src/database/entities/dns_provider.rs
Normal file
156
src/database/entities/dns_provider.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
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 = "dns_providers")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
pub name: String,
|
||||
|
||||
pub provider_type: String, // "cloudflare", "route53", etc.
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub api_token: String, // Encrypted storage in production
|
||||
|
||||
pub is_active: bool,
|
||||
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DTOs for API requests/responses
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateDnsProviderDto {
|
||||
pub name: String,
|
||||
pub provider_type: String,
|
||||
pub api_token: String,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateDnsProviderDto {
|
||||
pub name: Option<String>,
|
||||
pub api_token: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DnsProviderResponseDto {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub provider_type: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub has_token: bool, // Don't expose actual token
|
||||
}
|
||||
|
||||
impl From<CreateDnsProviderDto> for ActiveModel {
|
||||
fn from(dto: CreateDnsProviderDto) -> Self {
|
||||
ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
name: Set(dto.name),
|
||||
provider_type: Set(dto.provider_type),
|
||||
api_token: Set(dto.api_token),
|
||||
is_active: Set(dto.is_active.unwrap_or(true)),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Update this model with data from UpdateDnsProviderDto
|
||||
pub fn apply_update(self, dto: UpdateDnsProviderDto) -> ActiveModel {
|
||||
let mut active_model: ActiveModel = self.into();
|
||||
|
||||
if let Some(name) = dto.name {
|
||||
active_model.name = Set(name);
|
||||
}
|
||||
if let Some(api_token) = dto.api_token {
|
||||
active_model.api_token = Set(api_token);
|
||||
}
|
||||
if let Some(is_active) = dto.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
active_model
|
||||
}
|
||||
|
||||
/// Convert to response DTO (without exposing API token)
|
||||
pub fn to_response_dto(&self) -> DnsProviderResponseDto {
|
||||
DnsProviderResponseDto {
|
||||
id: self.id,
|
||||
name: self.name.clone(),
|
||||
provider_type: self.provider_type.clone(),
|
||||
is_active: self.is_active,
|
||||
created_at: self.created_at,
|
||||
updated_at: self.updated_at,
|
||||
has_token: !self.api_token.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported DNS provider types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DnsProviderType {
|
||||
#[serde(rename = "cloudflare")]
|
||||
Cloudflare,
|
||||
}
|
||||
|
||||
impl DnsProviderType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
DnsProviderType::Cloudflare => "cloudflare",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"cloudflare" => Some(DnsProviderType::Cloudflare),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all() -> Vec<Self> {
|
||||
vec![DnsProviderType::Cloudflare]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod user;
|
||||
pub mod certificate;
|
||||
pub mod dns_provider;
|
||||
pub mod inbound_template;
|
||||
pub mod server;
|
||||
pub mod server_inbound;
|
||||
@@ -7,7 +8,9 @@ pub mod user_access;
|
||||
pub mod inbound_users;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::user::Entity as User;
|
||||
pub use super::certificate::Entity as Certificate;
|
||||
pub use super::dns_provider::Entity as DnsProvider;
|
||||
pub use super::inbound_template::Entity as InboundTemplate;
|
||||
pub use super::server::Entity as Server;
|
||||
pub use super::server_inbound::Entity as ServerInbound;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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(DnsProviders::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::Name)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::ProviderType)
|
||||
.string_len(50)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::ApiToken)
|
||||
.text()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::IsActive)
|
||||
.boolean()
|
||||
.default(true)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(DnsProviders::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Index on name for faster lookups
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_dns_providers_name")
|
||||
.table(DnsProviders::Table)
|
||||
.col(DnsProviders::Name)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.if_exists()
|
||||
.name("idx_dns_providers_name")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_table(Table::drop().table(DnsProviders::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum DnsProviders {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
ProviderType,
|
||||
ApiToken,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
@@ -9,6 +9,7 @@ mod m20241201_000006_create_user_access_table;
|
||||
mod m20241201_000007_create_inbound_users_table;
|
||||
mod m20250919_000001_update_inbound_users_schema;
|
||||
mod m20250922_000001_add_grpc_hostname_to_servers;
|
||||
mod m20250923_000001_create_dns_providers_table;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -25,6 +26,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20241201_000007_create_inbound_users_table::Migration),
|
||||
Box::new(m20250919_000001_update_inbound_users_schema::Migration),
|
||||
Box::new(m20250922_000001_add_grpc_hostname_to_servers::Migration),
|
||||
Box::new(m20250923_000001_create_dns_providers_table::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -72,4 +72,26 @@ impl CertificateRepository {
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Update certificate data (cert and key) and expiration date
|
||||
pub async fn update_certificate_data(
|
||||
&self,
|
||||
id: Uuid,
|
||||
cert_pem: &str,
|
||||
key_pem: &str,
|
||||
expires_at: chrono::DateTime<chrono::Utc>
|
||||
) -> Result<certificate::Model> {
|
||||
let mut cert: certificate::ActiveModel = Certificate::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Certificate not found"))?
|
||||
.into();
|
||||
|
||||
cert.cert_data = Set(cert_pem.as_bytes().to_vec());
|
||||
cert.key_data = Set(key_pem.as_bytes().to_vec());
|
||||
cert.expires_at = Set(expires_at);
|
||||
cert.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(cert.update(&self.db).await?)
|
||||
}
|
||||
}
|
||||
132
src/database/repository/dns_provider.rs
Normal file
132
src/database/repository/dns_provider.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter, Set, PaginatorTrait};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::dns_provider::{
|
||||
Entity, Model, ActiveModel, CreateDnsProviderDto, UpdateDnsProviderDto, Column, DnsProviderType
|
||||
};
|
||||
|
||||
pub struct DnsProviderRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl DnsProviderRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn find_all(&self) -> Result<Vec<Model>> {
|
||||
let providers = Entity::find().all(&self.db).await?;
|
||||
Ok(providers)
|
||||
}
|
||||
|
||||
pub async fn find_active(&self) -> Result<Vec<Model>> {
|
||||
let providers = Entity::find()
|
||||
.filter(Column::IsActive.eq(true))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(providers)
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let provider = Entity::find_by_id(id).one(&self.db).await?;
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
pub async fn find_by_name(&self, name: &str) -> Result<Option<Model>> {
|
||||
let provider = Entity::find()
|
||||
.filter(Column::Name.eq(name))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
pub async fn find_by_type(&self, provider_type: &str) -> Result<Vec<Model>> {
|
||||
let providers = Entity::find()
|
||||
.filter(Column::ProviderType.eq(provider_type))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(providers)
|
||||
}
|
||||
|
||||
pub async fn find_active_by_type(&self, provider_type: &str) -> Result<Vec<Model>> {
|
||||
let providers = Entity::find()
|
||||
.filter(Column::ProviderType.eq(provider_type))
|
||||
.filter(Column::IsActive.eq(true))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(providers)
|
||||
}
|
||||
|
||||
pub async fn create(&self, dto: CreateDnsProviderDto) -> Result<Model> {
|
||||
let active_model: ActiveModel = dto.into();
|
||||
let provider = active_model.insert(&self.db).await?;
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, dto: UpdateDnsProviderDto) -> Result<Option<Model>> {
|
||||
let provider = match self.find_by_id(id).await? {
|
||||
Some(provider) => provider,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let updated_model = provider.apply_update(dto);
|
||||
let updated_provider = updated_model.update(&self.db).await?;
|
||||
Ok(Some(updated_provider))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub async fn enable(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let provider = match self.find_by_id(id).await? {
|
||||
Some(provider) => provider,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let mut active_model: ActiveModel = provider.into();
|
||||
active_model.is_active = Set(true);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
let updated_provider = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated_provider))
|
||||
}
|
||||
|
||||
pub async fn disable(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let provider = match self.find_by_id(id).await? {
|
||||
Some(provider) => provider,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let mut active_model: ActiveModel = provider.into();
|
||||
active_model.is_active = Set(false);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
let updated_provider = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated_provider))
|
||||
}
|
||||
|
||||
/// Check if a provider name already exists
|
||||
pub async fn name_exists(&self, name: &str, exclude_id: Option<Uuid>) -> Result<bool> {
|
||||
let mut query = Entity::find().filter(Column::Name.eq(name));
|
||||
|
||||
if let Some(id) = exclude_id {
|
||||
query = query.filter(Column::Id.ne(id));
|
||||
}
|
||||
|
||||
let count = query.count(&self.db).await?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Get the first active provider of a specific type
|
||||
pub async fn get_active_provider_by_type(&self, provider_type: DnsProviderType) -> Result<Option<Model>> {
|
||||
let provider = Entity::find()
|
||||
.filter(Column::ProviderType.eq(provider_type.as_str()))
|
||||
.filter(Column::IsActive.eq(true))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
Ok(provider)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod user;
|
||||
pub mod certificate;
|
||||
pub mod dns_provider;
|
||||
pub mod inbound_template;
|
||||
pub mod server;
|
||||
pub mod server_inbound;
|
||||
@@ -8,6 +9,7 @@ pub mod inbound_users;
|
||||
|
||||
pub use user::UserRepository;
|
||||
pub use certificate::CertificateRepository;
|
||||
pub use dns_provider::DnsProviderRepository;
|
||||
pub use inbound_template::InboundTemplateRepository;
|
||||
pub use server::ServerRepository;
|
||||
pub use server_inbound::ServerInboundRepository;
|
||||
|
||||
@@ -107,6 +107,13 @@ impl ServerInboundRepository {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_certificate_id(&self, certificate_id: Uuid) -> Result<Vec<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find()
|
||||
.filter(server_inbound::Column::CertificateId.eq(certificate_id))
|
||||
.all(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn find_active_by_server(&self, server_id: Uuid) -> Result<Vec<server_inbound::Model>> {
|
||||
Ok(ServerInbound::find()
|
||||
.filter(server_inbound::Column::ServerId.eq(server_id))
|
||||
|
||||
Reference in New Issue
Block a user