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::{
            CreateRecordRequest, ErrorResponse, GetRecordResponse, GetRecordsFilter,
            MessageResponse, RecordListResponse, RecordResponse, UpdateRecordRequest,
        },
    },
    service::record::RecordService,
};

pub(crate) struct RecordApi;

impl RecordApi {
    pub(crate) async fn routes() -> Router {
        Router::new()
            .route("/records", routing::get(get_records))
            .route("/records/{record_id}", routing::get(get_record))
            .route("/records", routing::post(create_record))
            .route("/records/{record_id}", routing::put(update_record))
            .route("/records/{record_id}", routing::delete(delete_record))
    }
}

#[utoipa::path(
        get,
        path = "/records",
        tag = "Record",
        summary = "List all DNS records",
        params(
            ("zone_name" = Option<String>, Query, description = "The name of the DNS zone to filter records by."),
            ("name" = Option<String>, Query, description = "Filter by record name."),
            ("record_type" = Option<String>, Query, description = "Filter by record type."),
            ("value" = Option<String>, Query, description = "Partially filter by record value."),
            ("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."),
            ("priority" = Option<i32>, Query, description = "Filter by priority."),
            ("min_priority" = Option<i32>, Query, description = "Filter by minimum priority."),
            ("max_priority" = Option<i32>, Query, description = "Filter by maximum priority."),
            ("search" = Option<String>, Query, description = "Partially search records."),
            ("limit" = Option<u32>, Query, description = "Maximum number of records to return."),
            ("offset" = Option<u64>, Query, description = "Number of records to skip.")
        ),
        responses(
            (status = 200, description = "A list of DNS records", body = RecordListResponse),
            (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_records(Query(query): Query<GetRecordsFilter>) -> impl IntoResponse {
    let raw_records = match RecordService::list_with_zone_by_filter(query).await {
        Ok(records) => records,
        Err(err) => return ApiError::from(err).into_response(),
    };

    let records = raw_records
        .items
        .iter()
        .map(GetRecordResponse::from_record_with_zone)
        .collect::<Vec<_>>();

    let json_body = json!({ "items": records, "pagination": raw_records.pagination });
    (StatusCode::OK, Json(json_body)).into_response()
}

#[utoipa::path(
        get,
        path = "/records/{record_id}",
        tag = "Record",
        summary = "Get a specific DNS record",
        params(
            ("record_id" = i32, Path, description = "The ID of the DNS record to retrieve.")
        ),
        responses(
            (status = 200, description = "Details of the DNS record", body = RecordResponse),
            (status = 401, description = "Unauthorized", body = ErrorResponse),
            (status = 404, description = "Record not found", body = ErrorResponse),
            (status = 500, description = "Internal server error", body = ErrorResponse)
        )
)]
pub(crate) async fn get_record(Path(params): Path<GetRecordParam>) -> impl IntoResponse {
    let raw_record = match RecordService::get_by_id_with_zone(params.record_id).await {
        Ok(record) => record,
        Err(err) => return ApiError::from(err).into_response(),
    };

    let record = GetRecordResponse::from_record_with_zone(&raw_record);

    let json_body = json!({ "record": record });
    (StatusCode::OK, Json(json_body)).into_response()
}

#[utoipa::path(
        post,
        path = "/records",
        tag = "Record",
        summary = "Create a new DNS record",
        request_body = CreateRecordRequest,
        responses(
            (status = 201, description = "DNS record created successfully", body = RecordResponse),
            (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_record(
    JsonBody(body): JsonBody<CreateRecordRequest>,
) -> impl IntoResponse {
    let raw_record = match RecordService::create(&body).await {
        Ok(record) => record,
        Err(err) => return ApiError::from(err).into_response(),
    };

    let record = GetRecordResponse::from_record_with_zone(&raw_record);

    let json_body = json!({ "record": record });
    (StatusCode::CREATED, Json(json_body)).into_response()
}

#[utoipa::path(
        put,
        path = "/records/{record_id}",
        tag = "Record",
        summary = "Update a specific DNS record",
        params(
            ("record_id" = i32, Path, description = "The ID of the DNS record to update.")
        ),
        request_body = UpdateRecordRequest,
        responses(
            (status = 200, description = "DNS record updated successfully", body = RecordResponse),
            (status = 400, description = "Bad request, invalid input", body = ErrorResponse),
            (status = 401, description = "Unauthorized", body = ErrorResponse),
            (status = 404, description = "Record 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_record(
    Path(params): Path<UpdateRecordParam>,
    JsonBody(body): JsonBody<UpdateRecordRequest>,
) -> impl IntoResponse {
    let raw_record = match RecordService::update_by_id(params.record_id, &body).await {
        Ok(record) => record,
        Err(err) => return ApiError::from(err).into_response(),
    };

    let record = GetRecordResponse::from_record_with_zone(&raw_record);

    let json_body = json!({ "record": record });
    (StatusCode::OK, Json(json_body)).into_response()
}

#[utoipa::path(
        delete,
        path = "/records/{record_id}",
        tag = "Record",
        summary = "Delete a specific DNS record",
        params(
            ("record_id" = i32, Path, description = "The ID of the DNS record to delete.")
        ),
        responses(
            (status = 200, description = "DNS record deleted successfully", body = MessageResponse),
            (status = 401, description = "Unauthorized", body = ErrorResponse),
            (status = 404, description = "Record not found", body = ErrorResponse),
            (status = 500, description = "Internal server error", body = ErrorResponse)
        )
)]
pub(crate) async fn delete_record(Path(params): Path<DeleteRecordParam>) -> impl IntoResponse {
    match RecordService::delete_by_id(params.record_id).await {
        Ok(_) => {
            let json_body = json!({ "message": "Record deleted successfully" });
            (StatusCode::OK, Json(json_body)).into_response()
        }
        Err(err) => ApiError::from(err).into_response(),
    }
}

#[derive(Debug, Deserialize)]
pub(crate) struct GetRecordParam {
    record_id: i32,
}

#[derive(Debug, Deserialize)]
pub(crate) struct UpdateRecordParam {
    record_id: i32,
}

#[derive(Debug, Deserialize)]
pub(crate) struct DeleteRecordParam {
    record_id: i32,
}