Letsencrypt works

This commit is contained in:
Ultradesu
2025-09-24 00:30:03 +01:00
parent 59b8cbb582
commit 76afa0797b
26 changed files with 3169 additions and 60 deletions

View File

@@ -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)]

View 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]
}
}

View File

@@ -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;

View File

@@ -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,
}

View File

@@ -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),
]
}
}

View File

@@ -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?)
}
}

View 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)
}
}

View File

@@ -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;

View File

@@ -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))