bindizr 0.1.0-beta.4

DNS Synchronization Service for BIND9
Documentation
use axum::{
    Json, Router,
    extract::{Path, Query},
    http::StatusCode,
    response::IntoResponse,
    routing,
};
use serde::Deserialize;
use serde_json::json;

use crate::{
    api::{
        error::ApiError,
        middleware::body_parser::JsonBody,
        types::{
            CreateZoneRequest, ErrorResponse, GetRecordResponse, GetZoneResponse, GetZonesFilter,
            MessageResponse, ZoneDetailResponse, ZoneListResponse, ZoneResponse,
        },
    },
    service::{record::RecordService, zone::ZoneService},
};

pub(crate) struct ZoneApi;

impl ZoneApi {
    pub(crate) async fn routes() -> Router {
        Router::new()
            .route("/zones", routing::get(get_zones))
            .route("/zones/{name}", routing::get(get_zone))
            .route("/zones", routing::post(create_zone))
            .route("/zones/{name}", routing::put(update_zone))
            .route("/zones/{name}", routing::delete(delete_zone))
    }
}

#[utoipa::path(
        get,
        path = "/zones",
        tag = "Zone",
        summary = "List all DNS zones",
        params(
            ("name" = Option<String>, Query, description = "Filter by zone name."),
            ("id" = Option<i32>, Query, description = "Filter by zone ID."),
            ("primary_ns" = Option<String>, Query, description = "Filter by primary name server."),
            ("admin_email" = Option<String>, Query, description = "Filter by admin email."),
            ("ttl" = Option<i32>, Query, description = "Filter by TTL."),
            ("min_ttl" = Option<i32>, Query, description = "Filter by minimum TTL."),
            ("max_ttl" = Option<i32>, Query, description = "Filter by maximum TTL."),
            ("serial" = Option<i32>, Query, description = "Filter by serial."),
            ("search" = Option<String>, Query, description = "Partially search zones."),
            ("limit" = Option<u32>, Query, description = "Maximum number of zones to return."),
            ("offset" = Option<u64>, Query, description = "Number of zones to skip.")
        ),
        responses(
            (status = 200, description = "A list of DNS zones", body = ZoneListResponse),
            (status = 400, description = "Bad request, invalid pagination", body = ErrorResponse),
            (status = 401, description = "Unauthorized", body = ErrorResponse),
            (status = 500, description = "Internal server error", body = ErrorResponse)
        )
)]
pub(crate) async fn get_zones(Query(query): Query<GetZonesFilter>) -> impl IntoResponse {
    match ZoneService::list_by_filter(query).await {
        Ok(response) => {
            let zones = response
                .items
                .iter()
                .map(GetZoneResponse::from_zone)
                .collect::<Vec<GetZoneResponse>>();
            let json_body = json!({ "items": zones, "pagination": response.pagination });
            (StatusCode::OK, Json(json_body)).into_response()
        }
        Err(err) => ApiError::from(err).into_response(),
    }
}

#[utoipa::path(
        get,
        path = "/zones/{name}",
        tag = "Zone",
        summary = "Get a specific DNS zone",
        params(
            ("name" = String, Path, description = "The name of the DNS zone to retrieve."),
            ("records" = Option<bool>, Query, description = "Whether to include records for the DNS zone.")
        ),
        responses(
            (status = 200, description = "Details of the DNS zone", body = ZoneDetailResponse),
            (status = 401, description = "Unauthorized", body = ErrorResponse),
            (status = 404, description = "Zone not found", body = ErrorResponse),
            (status = 500, description = "Internal server error", body = ErrorResponse)
        )
)]
pub(crate) async fn get_zone(
    Path(params): Path<GetZoneParam>,
    Query(query): Query<GetZoneQuery>,
) -> impl IntoResponse {
    let zone_name = params.name;
    let records_query = query.records;

    let raw_zone = match ZoneService::get_by_name(&zone_name).await {
        Ok(zone) => zone,
        Err(err) => return ApiError::from(err).into_response(),
    };

    let raw_records = match records_query {
        Some(true) => match RecordService::list(Some(raw_zone.name.clone())).await {
            Ok(records) => records,
            Err(err) => return ApiError::from(err).into_response(),
        },
        _ => vec![],
    };
    let records = raw_records
        .iter()
        .map(|record| GetRecordResponse::from_record_and_zone_name(record, &raw_zone.name))
        .collect::<Vec<GetRecordResponse>>();

    let zone = GetZoneResponse::from_zone(&raw_zone);
    let json_body = json!({ "zone": zone, "records": records });
    (StatusCode::OK, Json(json_body)).into_response()
}

#[utoipa::path(
        post,
        path = "/zones",
        tag = "Zone",
        summary = "Create a new DNS zone",
        request_body = CreateZoneRequest,
        responses(
            (status = 201, description = "DNS zone created successfully", body = ZoneResponse),
            (status = 400, description = "Bad request, invalid input", body = ErrorResponse),
            (status = 401, description = "Unauthorized", body = ErrorResponse),
            (status = 415, description = "Unsupported media type, expected JSON request body", body = ErrorResponse),
            (status = 500, description = "Internal server error", body = ErrorResponse)
        )
)]
pub(crate) async fn create_zone(JsonBody(body): JsonBody<CreateZoneRequest>) -> impl IntoResponse {
    match ZoneService::create(&body).await {
        Ok(zone) => {
            let zone = GetZoneResponse::from_zone(&zone);
            let json_body = json!({ "zone": zone });
            (StatusCode::CREATED, Json(json_body)).into_response()
        }
        Err(err) => ApiError::from(err).into_response(),
    }
}

#[utoipa::path(
        put,
        path = "/zones/{name}",
        tag = "Zone",
        summary = "Update a specific DNS zone",
        params(
            ("name" = String, Path, description = "The name of the DNS zone to update.")
        ),
        request_body = CreateZoneRequest,
        responses(
            (status = 200, description = "DNS zone updated successfully", body = ZoneResponse),
            (status = 400, description = "Bad request, invalid input", body = ErrorResponse),
            (status = 401, description = "Unauthorized", body = ErrorResponse),
            (status = 404, description = "Zone not found", body = ErrorResponse),
            (status = 415, description = "Unsupported media type, expected JSON request body", body = ErrorResponse),
            (status = 500, description = "Internal server error", body = ErrorResponse)
        )
)]
pub(crate) async fn update_zone(
    Path(params): Path<UpdateZoneParam>,
    JsonBody(body): JsonBody<CreateZoneRequest>,
) -> impl IntoResponse {
    let zone_name = params.name;

    match ZoneService::update(&zone_name, &body).await {
        Ok(zone) => {
            let zone = GetZoneResponse::from_zone(&zone);
            let json_body = json!({ "zone": zone });
            (StatusCode::OK, Json(json_body)).into_response()
        }
        Err(err) => ApiError::from(err).into_response(),
    }
}

#[utoipa::path(
        delete,
        path = "/zones/{name}",
        tag = "Zone",
        summary = "Delete a specific DNS zone",
        params(
            ("name" = String, Path, description = "The name of the DNS zone to delete.")
        ),
        responses(
            (status = 200, description = "DNS zone deleted successfully", body = MessageResponse),
            (status = 401, description = "Unauthorized", body = ErrorResponse),
            (status = 404, description = "Zone not found", body = ErrorResponse),
            (status = 500, description = "Internal server error", body = ErrorResponse)
        )
)]
pub(crate) async fn delete_zone(Path(params): Path<DeleteZoneParam>) -> impl IntoResponse {
    let zone_name = params.name;

    match ZoneService::delete(&zone_name).await {
        Ok(_) => {
            let json_body = json!({ "message": "Zone deleted successfully" });
            (StatusCode::OK, Json(json_body)).into_response()
        }
        Err(err) => ApiError::from(err).into_response(),
    }
}

#[derive(Debug, Deserialize)]
pub(crate) struct GetZoneParam {
    name: String,
}

#[derive(Debug, Deserialize)]
pub(crate) struct GetZoneQuery {
    records: Option<bool>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct UpdateZoneParam {
    name: String,
}

#[derive(Debug, Deserialize)]
pub(crate) struct DeleteZoneParam {
    name: String,
}