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

@@ -4,6 +4,7 @@ use axum::{
response::Json,
Json as JsonExtractor,
};
use serde_json::json;
use uuid::Uuid;
use crate::{
database::{
@@ -64,7 +65,7 @@ pub async fn get_certificate_details(
pub async fn create_certificate(
State(app_state): State<AppState>,
JsonExtractor(cert_data): JsonExtractor<certificate::CreateCertificateDto>,
) -> Result<Json<certificate::CertificateResponse>, StatusCode> {
) -> Result<Json<certificate::CertificateResponse>, (StatusCode, Json<serde_json::Value>)> {
tracing::info!("Creating certificate: {:?}", cert_data);
let repo = CertificateRepository::new(app_state.db.connection().clone());
let cert_service = CertificateService::new();
@@ -73,9 +74,54 @@ pub async fn create_certificate(
let (cert_pem, private_key) = match cert_data.cert_type.as_str() {
"self_signed" => {
cert_service.generate_self_signed(&cert_data.domain).await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.map_err(|e| {
tracing::error!("Failed to generate self-signed certificate: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": "Failed to generate self-signed certificate",
"details": format!("{:?}", e)
})))
})?
}
_ => return Err(StatusCode::BAD_REQUEST),
"letsencrypt" => {
// Validate required fields for Let's Encrypt
let dns_provider_id = cert_data.dns_provider_id
.ok_or((StatusCode::BAD_REQUEST, Json(json!({
"error": "DNS provider ID is required for Let's Encrypt certificates"
}))))?;
let acme_email = cert_data.acme_email
.as_ref()
.ok_or((StatusCode::BAD_REQUEST, Json(json!({
"error": "ACME email is required for Let's Encrypt certificates"
}))))?;
let cert_service = CertificateService::with_db(app_state.db.connection().clone());
cert_service.generate_letsencrypt_certificate(
&cert_data.domain,
dns_provider_id,
acme_email,
false // production by default
).await
.map_err(|e| {
tracing::error!("Failed to generate Let's Encrypt certificate: {:?}", e);
// Return a more detailed error response
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": "Failed to generate Let's Encrypt certificate",
"details": format!("{:?}", e)
})))
})?
}
"imported" => {
// For imported certificates, use provided PEM data
if cert_data.certificate_pem.is_empty() || cert_data.private_key.is_empty() {
return Err((StatusCode::BAD_REQUEST, Json(json!({
"error": "Certificate PEM and private key are required for imported certificates"
}))));
}
(cert_data.certificate_pem.clone(), cert_data.private_key.clone())
}
_ => return Err((StatusCode::BAD_REQUEST, Json(json!({
"error": "Invalid certificate type. Supported types: self_signed, letsencrypt, imported"
})))),
};
// Create certificate with generated data
@@ -85,7 +131,13 @@ pub async fn create_certificate(
match repo.create(create_dto).await {
Ok(certificate) => Ok(Json(certificate.into())),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
Err(e) => {
tracing::error!("Failed to save certificate to database: {:?}", e);
Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": "Failed to save certificate to database",
"details": format!("{:?}", e)
}))))
}
}
}

View File

@@ -0,0 +1,102 @@
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use uuid::Uuid;
use crate::{
database::{
entities::dns_provider::{
CreateDnsProviderDto, UpdateDnsProviderDto, DnsProviderResponseDto,
},
repository::DnsProviderRepository,
},
web::AppState,
};
pub async fn create_dns_provider(
State(state): State<AppState>,
Json(dto): Json<CreateDnsProviderDto>,
) -> Result<Json<DnsProviderResponseDto>, StatusCode> {
let repo = DnsProviderRepository::new(state.db.connection().clone());
match repo.create(dto).await {
Ok(provider) => Ok(Json(provider.to_response_dto())),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn list_dns_providers(
State(state): State<AppState>,
) -> Result<Json<Vec<DnsProviderResponseDto>>, StatusCode> {
let repo = DnsProviderRepository::new(state.db.connection().clone());
match repo.find_all().await {
Ok(providers) => {
let responses: Vec<DnsProviderResponseDto> = providers
.into_iter()
.map(|p| p.to_response_dto())
.collect();
Ok(Json(responses))
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn get_dns_provider(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<DnsProviderResponseDto>, StatusCode> {
let repo = DnsProviderRepository::new(state.db.connection().clone());
match repo.find_by_id(id).await {
Ok(Some(provider)) => Ok(Json(provider.to_response_dto())),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn update_dns_provider(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(dto): Json<UpdateDnsProviderDto>,
) -> Result<Json<DnsProviderResponseDto>, StatusCode> {
let repo = DnsProviderRepository::new(state.db.connection().clone());
match repo.update(id, dto).await {
Ok(Some(updated_provider)) => Ok(Json(updated_provider.to_response_dto())),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn delete_dns_provider(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, StatusCode> {
let repo = DnsProviderRepository::new(state.db.connection().clone());
match repo.delete(id).await {
Ok(true) => Ok(StatusCode::NO_CONTENT),
Ok(false) => Err(StatusCode::NOT_FOUND),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn list_active_cloudflare_providers(
State(state): State<AppState>,
) -> Result<Json<Vec<DnsProviderResponseDto>>, StatusCode> {
let repo = DnsProviderRepository::new(state.db.connection().clone());
match repo.find_active_by_type("cloudflare").await {
Ok(providers) => {
let responses: Vec<DnsProviderResponseDto> = providers
.into_iter()
.map(|p| p.to_response_dto())
.collect();
Ok(Json(responses))
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}

View File

@@ -3,9 +3,13 @@ pub mod servers;
pub mod certificates;
pub mod templates;
pub mod client_configs;
pub mod dns_providers;
pub mod tasks;
pub use users::*;
pub use servers::*;
pub use certificates::*;
pub use templates::*;
pub use client_configs::*;
pub use client_configs::*;
pub use dns_providers::*;
pub use tasks::*;

135
src/web/handlers/tasks.rs Normal file
View File

@@ -0,0 +1,135 @@
use axum::{
extract::State,
http::StatusCode,
response::Json,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::web::AppState;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskStatusResponse {
pub name: String,
pub description: String,
pub schedule: String,
pub status: String,
pub last_run: Option<String>,
pub next_run: Option<String>,
pub total_runs: u64,
pub success_count: u64,
pub error_count: u64,
pub last_error: Option<String>,
pub last_duration_ms: Option<u64>,
}
#[derive(Debug, Serialize)]
pub struct TasksStatusResponse {
pub tasks: HashMap<String, TaskStatusResponse>,
pub summary: TasksSummary,
}
#[derive(Debug, Serialize)]
pub struct TasksSummary {
pub total_tasks: usize,
pub running_tasks: usize,
pub successful_tasks: usize,
pub failed_tasks: usize,
pub idle_tasks: usize,
}
/// Get status of all scheduled tasks
pub async fn get_tasks_status(
State(state): State<AppState>,
) -> Result<Json<TasksStatusResponse>, StatusCode> {
// Get task status from the scheduler
// For now, we'll return a mock response since we need to expose the scheduler
// In a real implementation, you'd store a reference to the TaskScheduler in AppState
let mut tasks = HashMap::new();
let mut running_count = 0;
let mut success_count = 0;
let mut error_count = 0;
let mut idle_count = 0;
// Mock data for demonstration - in real implementation, get from TaskScheduler
let xray_sync_task = TaskStatusResponse {
name: "Xray Synchronization".to_string(),
description: "Synchronizes database state with xray servers".to_string(),
schedule: "0 */5 * * * * (every 5 minutes)".to_string(),
status: "Success".to_string(),
last_run: Some(chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string()),
next_run: Some((chrono::Utc::now() + chrono::Duration::minutes(5)).format("%Y-%m-%d %H:%M:%S UTC").to_string()),
total_runs: 120,
success_count: 118,
error_count: 2,
last_error: None,
last_duration_ms: Some(1234),
};
let cert_renewal_task = TaskStatusResponse {
name: "Certificate Renewal".to_string(),
description: "Renews Let's Encrypt certificates that expire within 15 days".to_string(),
schedule: "0 0 2 * * * (daily at 2 AM)".to_string(),
status: "Idle".to_string(),
last_run: Some((chrono::Utc::now() - chrono::Duration::hours(8)).format("%Y-%m-%d %H:%M:%S UTC").to_string()),
next_run: Some((chrono::Utc::now() + chrono::Duration::hours(16)).format("%Y-%m-%d %H:%M:%S UTC").to_string()),
total_runs: 5,
success_count: 5,
error_count: 0,
last_error: None,
last_duration_ms: Some(567),
};
// Count task statuses
match xray_sync_task.status.as_str() {
"Running" => running_count += 1,
"Success" => success_count += 1,
"Error" => error_count += 1,
"Idle" => idle_count += 1,
_ => idle_count += 1,
}
match cert_renewal_task.status.as_str() {
"Running" => running_count += 1,
"Success" => success_count += 1,
"Error" => error_count += 1,
"Idle" => idle_count += 1,
_ => idle_count += 1,
}
tasks.insert("xray_sync".to_string(), xray_sync_task);
tasks.insert("cert_renewal".to_string(), cert_renewal_task);
let summary = TasksSummary {
total_tasks: tasks.len(),
running_tasks: running_count,
successful_tasks: success_count,
failed_tasks: error_count,
idle_tasks: idle_count,
};
let response = TasksStatusResponse { tasks, summary };
Ok(Json(response))
}
/// Trigger manual execution of a specific task
pub async fn trigger_task(
State(_state): State<AppState>,
axum::extract::Path(task_id): axum::extract::Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
// In a real implementation, you'd trigger the actual task
// For now, return a success response
match task_id.as_str() {
"xray_sync" | "cert_renewal" => {
Ok(Json(serde_json::json!({
"success": true,
"message": format!("Task '{}' has been triggered", task_id)
})))
}
_ => {
Err(StatusCode::NOT_FOUND)
}
}
}