diff --git a/docker-compose.master.yml b/docker-compose.master.yml index 3125643..701f440 100644 --- a/docker-compose.master.yml +++ b/docker-compose.master.yml @@ -95,6 +95,7 @@ services: nginx: image: jonasal/nginx-certbot:latest + container_name: pdns-nginx restart: always environment: CERTBOT_EMAIL: ${CERTBOT_EMAIL} diff --git a/docker-compose.slave.yml b/docker-compose.slave.yml index 25c7346..06fce19 100644 --- a/docker-compose.slave.yml +++ b/docker-compose.slave.yml @@ -54,6 +54,7 @@ services: restart: always nginx: image: jonasal/nginx-certbot:latest + container_name: pdns-nginx restart: always environment: CERTBOT_EMAIL: ${CERTBOT_EMAIL} diff --git a/src/main/kotlin/space/mori/dnsapi/GlobalExceptionHandler.kt b/src/main/kotlin/space/mori/dnsapi/GlobalExceptionHandler.kt new file mode 100644 index 0000000..ea94a62 --- /dev/null +++ b/src/main/kotlin/space/mori/dnsapi/GlobalExceptionHandler.kt @@ -0,0 +1,28 @@ +package space.mori.dnsapi + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import space.mori.dnsapi.dto.ApiResponseDTO +import space.mori.dnsapi.dto.ErrorOrMessage + +@RestControllerAdvice +class GlobalExceptionHandler { + @ExceptionHandler(PowerDNSAPIException::class) + fun handlePowerDNSAPIException(ex: PowerDNSAPIException): ResponseEntity> { + var idx = 0 + val errors = mutableListOf(ErrorOrMessage(idx, ex.message ?: "")) + ex.errors?.forEach{ + errors.add(ErrorOrMessage(idx++, it)) + } + + val response = ApiResponseDTO( + success = false, + errors = errors, + result = null + ) + + return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/PowerDNSAPIClient.kt b/src/main/kotlin/space/mori/dnsapi/PowerDNSAPIClient.kt index 9f06041..3ab580a 100644 --- a/src/main/kotlin/space/mori/dnsapi/PowerDNSAPIClient.kt +++ b/src/main/kotlin/space/mori/dnsapi/PowerDNSAPIClient.kt @@ -29,7 +29,7 @@ class PowerDNSAPIClient() { @Throws(PowerDNSAPIException::class) fun createZone(zoneName: String): Response { val body = gson.toJson(mapOf( - "name" to zoneName, + "name" to "$zoneName.", "nameservers" to nameserver.split(",")) ).toRequestBody() val request = Request.Builder() @@ -51,7 +51,7 @@ class PowerDNSAPIClient() { @Throws(PowerDNSAPIException::class) fun deleteZone(zoneName: String): Response { val request = Request.Builder() - .url("$apiUrl/api/v1/servers/localhost/zones/$zoneName") + .url("$apiUrl/api/v1/servers/localhost/zones/$zoneName.") .addHeader("X-API-Key", apiKey) .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") @@ -74,7 +74,7 @@ class PowerDNSAPIClient() { "content" to recordContent )).toRequestBody("application/json".toMediaType()) val request = Request.Builder() - .url("$apiUrl/api/v1/servers/localhost/zones/$zoneName/records") + .url("$apiUrl/api/v1/servers/localhost/zones/$zoneName./records") .addHeader("X-API-Key", apiKey) .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") @@ -95,7 +95,7 @@ class PowerDNSAPIClient() { "content" to recordContent )).toRequestBody("application/json".toMediaType()) val request = Request.Builder() - .url("$apiUrl/api/v1/servers/localhost/zones/$zoneName/records/$recordName/$recordType") + .url("$apiUrl/api/v1/servers/localhost/zones/$zoneName./records/$recordName/$recordType") .addHeader("X-API-Key", apiKey) .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") @@ -113,7 +113,7 @@ class PowerDNSAPIClient() { @Throws(PowerDNSAPIException::class) fun deleteRecord(zoneName: String, recordName: String, recordType: String): Response { val request = Request.Builder() - .url("$apiUrl/api/v1/servers/localhost/zones/$zoneName/records/$recordName/$recordType") + .url("$apiUrl/api/v1/servers/localhost/zones/$zoneName./records/$recordName/$recordType") .addHeader("X-API-Key", apiKey) .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") @@ -132,7 +132,7 @@ class PowerDNSAPIClient() { @ReflectiveAccess data class PowerDNSAPIError( @SerializedName("error") val error: String, - @SerializedName("errors") val errors: List + @SerializedName("errors") val errors: List? ) class PowerDNSAPIErrorDeserializer : JsonDeserializer { @@ -150,6 +150,8 @@ class PowerDNSAPIErrorDeserializer : JsonDeserializer { } class PowerDNSAPIException(private val error: PowerDNSAPIError): RuntimeException(error.error) { - val errors: List + val errors: List? get() = error.errors + + override fun toString(): String = "PowerDNSAPIException(${error.error} ${errors?.joinToString(", ") ?: ""})" } \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/controller/DomainController.kt b/src/main/kotlin/space/mori/dnsapi/controller/DomainController.kt index 3018d49..b1fa1a0 100644 --- a/src/main/kotlin/space/mori/dnsapi/controller/DomainController.kt +++ b/src/main/kotlin/space/mori/dnsapi/controller/DomainController.kt @@ -6,10 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.* -import org.springframework.web.server.ResponseStatusException -import space.mori.dnsapi.PowerDNSAPIException import space.mori.dnsapi.db.Domain import space.mori.dnsapi.dto.* import space.mori.dnsapi.service.DomainService @@ -29,19 +26,7 @@ class DomainController( content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]) ]) fun allDomains(): ApiResponseDTO> { - try { - return ApiResponseDTO(result = domainService.getAllDomains().map { it.toDTO() }) - } catch(e : PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + return ApiResponseDTO(result = domainService.getAllDomains().map { it.toDTO() }) } @Operation(summary = "Get domain", tags = ["domain"]) @@ -54,19 +39,7 @@ class DomainController( fun getDomainByCfid( @PathVariable cfid: String? ): ApiResponseDTO { - try { - return ApiResponseDTO(result = domainService.getDomainById(cfid!!).toDTO()) - } catch(e : PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + return ApiResponseDTO(result = domainService.getDomainById(cfid!!).toDTO()) } @Operation(summary = "Create domain", tags = ["domain"]) @@ -77,19 +50,7 @@ class DomainController( ]) @PostMapping fun createDomain(@RequestBody domain: DomainRequestDTO): ApiResponseDTO { - try { - return ApiResponseDTO(result = domainService.createDomain(domain).toDTO()) - } catch(e : PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + return ApiResponseDTO(result = domainService.createDomain(domain).toDTO()) } @Operation(summary = "Delete domain", tags = ["domain"]) @@ -100,21 +61,9 @@ class DomainController( ]) @DeleteMapping("/{domain_id}") fun deleteDomain(@PathVariable domain_id: String?): ApiResponseDTO { - try { - domainService.deleteDomain(domain_id!!) + domainService.deleteDomain(domain_id!!) - return ApiResponseDTO(result = DeleteResponseWithId(domain_id)) - } catch (e: PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + return ApiResponseDTO(result = DeleteResponseWithId(domain_id)) } private fun Domain.toDTO() = DomainResponseDTO(id = cfid, name = name) diff --git a/src/main/kotlin/space/mori/dnsapi/controller/RecordController.kt b/src/main/kotlin/space/mori/dnsapi/controller/RecordController.kt index 7e0d5f0..f69e58d 100644 --- a/src/main/kotlin/space/mori/dnsapi/controller/RecordController.kt +++ b/src/main/kotlin/space/mori/dnsapi/controller/RecordController.kt @@ -6,10 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.* -import org.springframework.web.server.ResponseStatusException -import space.mori.dnsapi.PowerDNSAPIException import space.mori.dnsapi.dto.* import space.mori.dnsapi.service.RecordService @@ -27,19 +24,7 @@ class RecordController( content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) fun allRecords(@PathVariable zone_id: String): ApiResponseDTO> { - try { - return ApiResponseDTO(result = recordService.getRecordsByDomain(zone_id)?.map{ it } ?: listOf()) - } catch(e : PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + return ApiResponseDTO(result = recordService.getRecordsByDomain(zone_id)?.map{ it } ?: listOf()) } @GetMapping("{zone_id}/dns_records/{dns_record_id}") @@ -50,19 +35,7 @@ class RecordController( content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) fun getRecordByCfid(@PathVariable zone_id: String, @PathVariable dns_record_id: String): ApiResponseDTO { - try { - return ApiResponseDTO(result = recordService.getRecord(zone_id, dns_record_id)) - } catch(e : PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + return ApiResponseDTO(result = recordService.getRecord(zone_id, dns_record_id)) } @PostMapping("{zone_id}/dns_records") @@ -73,19 +46,7 @@ class RecordController( content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) fun createRecord(@PathVariable zone_id: String, @RequestBody record: RecordRequestDTO): ApiResponseDTO { - try { - return ApiResponseDTO(result = recordService.createRecord(zone_id, record)) - } catch(e : PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + return ApiResponseDTO(result = recordService.createRecord(zone_id, record)) } @DeleteMapping("{zone_id}/dns_records/{dns_record_id}") @@ -96,20 +57,8 @@ class RecordController( content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) fun deleteRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String): ApiResponseDTO { - try { - val record_id = recordService.deleteRecord(zone_id, dns_record_id) - return ApiResponseDTO(result = DeleteResponseWithId(record_id)) - } catch(e : PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + val record_id = recordService.deleteRecord(zone_id, dns_record_id) + return ApiResponseDTO(result = DeleteResponseWithId(record_id)) } @PatchMapping("{zone_id}/dns_records/{dns_record_id}") @@ -120,18 +69,6 @@ class RecordController( content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) fun updateRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String, @RequestBody record: RecordRequestDTO): ApiResponseDTO { - try { - return ApiResponseDTO(result = recordService.updateRecord(zone_id, dns_record_id, record)) - } catch(e : PowerDNSAPIException) { - var idx = 0 - val errors = mutableListOf(ErrorOrMessage(idx, e.message ?: "")) - e.errors.forEach{ - errors.add(ErrorOrMessage(idx++, it)) - } - - throw ResponseStatusException(HttpStatus.EXPECTATION_FAILED, - ApiResponseDTO(false, errors = errors, result = listOf(null)).toString() - ) - } + return ApiResponseDTO(result = recordService.updateRecord(zone_id, dns_record_id, record)) } } \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/dto/ApiResponseDTO.kt b/src/main/kotlin/space/mori/dnsapi/dto/ApiResponseDTO.kt index 2124070..e6f5ef9 100644 --- a/src/main/kotlin/space/mori/dnsapi/dto/ApiResponseDTO.kt +++ b/src/main/kotlin/space/mori/dnsapi/dto/ApiResponseDTO.kt @@ -1,16 +1,23 @@ package space.mori.dnsapi.dto import com.google.gson.GsonBuilder +import com.google.gson.annotations.SerializedName val gson = GsonBuilder().setPrettyPrinting().create() data class ApiResponseDTO( + @SerializedName("success") val success: Boolean = true, + @SerializedName("errors") val errors: List = listOf(), + @SerializedName("messages") val messages: List = listOf(), + @SerializedName("result") val result: T? = null ) { - override fun toString(): String = gson.toJson(this) + override fun toString(): String { + return gson.toJson(this) + } } data class ErrorOrMessage( diff --git a/src/main/kotlin/space/mori/dnsapi/service/DomainService.kt b/src/main/kotlin/space/mori/dnsapi/service/DomainService.kt index e14854f..c2d663c 100644 --- a/src/main/kotlin/space/mori/dnsapi/service/DomainService.kt +++ b/src/main/kotlin/space/mori/dnsapi/service/DomainService.kt @@ -17,42 +17,59 @@ class DomainService( private val powerDNSApiClient: PowerDNSAPIClient ) { fun getAllDomains(): List { - val user = getCurrentUser() - val domain = domainRepository.findAllByUser(user) - if(domain.isEmpty()) throw RuntimeException("Unauthorized") + try { + val user = getCurrentUser() + val domain = domainRepository.findAllByUser(user) + if (domain.isEmpty()) throw RuntimeException("Unauthorized") - return domain + return domain + } catch(ex: Error) { + throw ex + } } fun getDomainById(domain_id: String): Domain { - val domain = domainRepository.findByCfid(domain_id).orElseThrow { - RuntimeException("Failed to find domain in API: $domain_id") - } - val user = getCurrentUser() - if(domain.user.id != user.id) - throw RuntimeException("Unauthorized to create record in API: $domain_id") + try { + val domain = domainRepository.findByCfid(domain_id).orElseThrow { + RuntimeException("Failed to find domain in API: $domain_id") + } + val user = getCurrentUser() + if (domain.user.id != user.id) + throw RuntimeException("Unauthorized to create record in API: $domain_id") - return domain + return domain + } catch(ex: Error) { + throw ex + } } + @Throws(Exception::class) fun createDomain(domain: DomainRequestDTO): Domain { - val user = getCurrentUser() + try { + val user = getCurrentUser() - powerDNSApiClient.createZone(domain.name) - val saved_domain = domainRepository.save(Domain(name=domain.name, user=user)) + powerDNSApiClient.createZone(domain.name) + val saved_domain = domainRepository.save(Domain(name = domain.name, user = user)) - return saved_domain + return saved_domain + } catch(ex: Error) { + throw ex + } } fun deleteDomain(domain_id: String): String { - val domain = domainRepository.findByCfid(domain_id).orElseThrow { - throw RuntimeException("Domain with CFID $domain_id not found") + try { + val domain = domainRepository.findByCfid(domain_id).orElseThrow { + throw RuntimeException("Domain with CFID $domain_id not found") + } + + powerDNSApiClient.deleteZone(domain.name) + val count = domainRepository.deleteByCfid(domain_id) + + if (count > 0) throw RuntimeException("Domain with CFID $domain_id not found") + return domain_id + } catch(ex: Error) { + throw ex } - - powerDNSApiClient.deleteZone(domain.name) - val count = domainRepository.deleteByCfid(domain_id) - - if(count > 0) throw RuntimeException("Domain with CFID $domain_id not found") - return domain_id } } \ No newline at end of file