diff --git a/build.gradle.kts b/build.gradle.kts index 03b1379..9671f63 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2") + implementation("com.google.code.gson:gson:2.11.0") + implementation("io.github.cdimascio:dotenv-kotlin:6.4.1") runtimeOnly("org.mariadb.jdbc:mariadb-java-client") diff --git a/docker_build.sh b/docker_build.sh new file mode 100755 index 0000000..e65f1b9 --- /dev/null +++ b/docker_build.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# 현재 날짜와 시간을 YYMMDD(AM/PM)HHMM 형식으로 설정 +current_time=$(date +'%y%m%d%p%H%M') + +# Docker 이미지 빌드 명령 실행 +docker build --no-cache -t dalbodeule/dnsapi:$current_time -t dalbodeule/dnsapi:latest . \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/DnsapiApplication.kt b/src/main/kotlin/space/mori/dnsapi/DnsapiApplication.kt index a8aaf01..ad8e0bd 100644 --- a/src/main/kotlin/space/mori/dnsapi/DnsapiApplication.kt +++ b/src/main/kotlin/space/mori/dnsapi/DnsapiApplication.kt @@ -3,6 +3,10 @@ package space.mori.dnsapi import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import io.github.cdimascio.dotenv.dotenv +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.* @SpringBootApplication class DnsapiApplication @@ -13,7 +17,9 @@ fun main(args: Array) { "DB_PORT" to dotenv["DB_PORT"], "DB_NAME" to dotenv["DB_NAME"], "DB_USER" to dotenv["DB_USER"], - "DB_PASSWORD" to dotenv["DB_PASSWORD"] + "DB_PASSWORD" to dotenv["DB_PASSWORD"], + "PDNS_API_KEY" to dotenv["PDNS_API_KEY"], + "PDNS_API_URL" to dotenv["PDNS_API_URL"], ) runApplication(*args) { @@ -23,4 +29,9 @@ fun main(args: Array) { val dotenv = dotenv { ignoreIfMissing = true +} + +fun Date.getISOFormat(): String { + val offsetDateTime = OffsetDateTime.ofInstant(this.toInstant(), ZoneOffset.UTC) + return offsetDateTime.format(DateTimeFormatter.ISO_DATE_TIME) } \ 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 new file mode 100644 index 0000000..2282f55 --- /dev/null +++ b/src/main/kotlin/space/mori/dnsapi/PowerDNSAPIClient.kt @@ -0,0 +1,82 @@ +package space.mori.dnsapi + +import com.google.gson.Gson +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.* +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate +import space.mori.dnsapi.dto.RecordRequestDTO + +@Service +class PowerDNSApiClient { + @Value("\${pdns.api.url}") + private lateinit var apiUrl: String + + @Value("\${pdns.api.key}") + private lateinit var apiKey: String + + private val restTemplate = RestTemplate() + private val gson = Gson() + + private fun createHeaders(): HttpHeaders { + val headers = HttpHeaders() + headers.set("X-API-Key", apiKey) + headers.contentType = MediaType.APPLICATION_JSON + return headers + } + + fun createDomain(name: String): ResponseEntity { + val url = "$apiUrl/servers/localhost/zones" + val headers = createHeaders() + val domainRequest = DomainRequest("$name.", "Master", arrayOf(), arrayOf()) + val body = gson.toJson(domainRequest) + val entity = HttpEntity(body, headers) + return restTemplate.exchange(url, HttpMethod.POST, entity, String::class.java) + } + + fun createRecord(domainName: String, recordRequest: RecordRequestDTO): ResponseEntity { + val url = "$apiUrl/servers/localhost/zones/$domainName." + val headers = createHeaders() + val record = RecordRequest( + name = "${recordRequest.name}.$domainName.", + type = recordRequest.type, + ttl = recordRequest.ttl, + changetype = "REPLACE", + records = arrayOf(RecordContent(recordRequest.content, false)) + ) + val body = gson.toJson(RecordRequestWrapper(arrayOf(record))) + val entity = HttpEntity(body, headers) + return restTemplate.exchange(url, HttpMethod.PATCH, entity, String::class.java) + } + + fun deleteDomain(name: String): ResponseEntity { + val url = "$apiUrl/servers/localhost/zones/$name." + val headers = createHeaders() + val entity = HttpEntity(headers) + return restTemplate.exchange(url, HttpMethod.DELETE, entity, String::class.java) + } + + private data class DomainRequest( + val name: String, + val kind: String, + val masters: Array, + val nameservers: Array + ) + + private data class RecordRequestWrapper( + val rrsets: Array + ) + + private data class RecordRequest( + val name: String, + val type: String, + val ttl: Int, + val changetype: String, + val records: Array + ) + + private data class RecordContent( + val content: String, + val disabled: Boolean + ) +} \ 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 695b0f0..a0609a3 100644 --- a/src/main/kotlin/space/mori/dnsapi/controller/DomainController.kt +++ b/src/main/kotlin/space/mori/dnsapi/controller/DomainController.kt @@ -8,7 +8,6 @@ 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.web.ErrorResponse import org.springframework.web.bind.annotation.* import space.mori.dnsapi.db.Domain import space.mori.dnsapi.dto.DomainResponseDTO @@ -68,5 +67,5 @@ class DomainController { domainService!!.deleteDomain(cfid!!) } - private fun Domain.toDTO() = DomainResponseDTO(cfid = cfid!!, domainName = domainName!!) + 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 6ba3707..4a72387 100644 --- a/src/main/kotlin/space/mori/dnsapi/controller/RecordController.kt +++ b/src/main/kotlin/space/mori/dnsapi/controller/RecordController.kt @@ -7,20 +7,23 @@ 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.data.jpa.domain.AbstractPersistable_.id import org.springframework.web.bind.annotation.* -import space.mori.dnsapi.service.RecordService -import space.mori.dnsapi.db.Record +import space.mori.dnsapi.db.Domain +import space.mori.dnsapi.db.Record as DomainRecord import space.mori.dnsapi.dto.RecordRequestDTO import space.mori.dnsapi.dto.RecordResponseDTO +import space.mori.dnsapi.getISOFormat +import space.mori.dnsapi.service.RecordService import java.util.* @RestController -@RequestMapping("/record") -class RecordController { +@RequestMapping("/zones") +class RecordController( @Autowired - private val recordService: RecordService? = null - - @GetMapping + private val recordService: RecordService, +) { + @GetMapping("{zone_id}/dns_records") @Operation(summary = "Get all records", tags=["record"]) @ApiResponses(value = [ ApiResponse(responseCode = "200", description = "Return All Records", @@ -28,11 +31,11 @@ class RecordController { ApiResponse(responseCode = "400", description = "Bad request", content = [Content(schema = Schema(implementation = Void::class))]), ]) - fun allRecords(@PathVariable cfid: String?): List { - return recordService!!.getAllRecords(cfid!!).map{ it?.toDTO() } + fun allRecords(@PathVariable zone_id: String): List { + return recordService.getRecordsByDomain(zone_id)?.map{ it } ?: listOf() } - @GetMapping("/{cfid}") + @GetMapping("{zone_id}/dns_records/{dns_record_id}") @Operation(summary = "Get Record by ID", tags=["record"]) @ApiResponses(value = [ ApiResponse(responseCode = "200", description = "Return Record", @@ -40,11 +43,11 @@ class RecordController { ApiResponse(responseCode = "400", description = "Bad request", content = [Content(schema = Schema(implementation = Void::class))]) ]) - fun getRecordByCfid(@PathVariable cfid: String?): Optional { - return recordService!!.getRecordById(cfid!!).map { it.toDTO() } + fun getRecordByCfid(@PathVariable zone_id: String, @PathVariable dns_record_id: String): RecordResponseDTO { + return recordService.getRecord(zone_id, dns_record_id) } - @PostMapping + @PostMapping("{zone_id}/dns_records") @Operation(summary = "Add Record by ID", tags=["record"]) @ApiResponses(value = [ ApiResponse(responseCode = "200", description = "Return Record", @@ -52,11 +55,11 @@ class RecordController { ApiResponse(responseCode = "400", description = "Bad request", content = [Content(schema = Schema(implementation = Void::class))]), ]) - fun createRecord(@RequestBody record: RecordRequestDTO): RecordResponseDTO { - return recordService!!.createRecord(record).toDTO() + fun createRecord(@PathVariable zone_id: String, @RequestBody record: RecordRequestDTO): RecordResponseDTO { + return recordService.createRecord(zone_id, record) } - @DeleteMapping("/{cfid}") + @DeleteMapping("{zone_id}/dns_records/{dns_record_id}") @Operation(summary = "Remove Record by ID", tags=["record"]) @ApiResponses(value = [ ApiResponse(responseCode = "200", description = "Return Record", @@ -64,19 +67,33 @@ class RecordController { ApiResponse(responseCode = "400", description = "Bad request", content = [Content(schema = Schema(implementation = Void::class))]), ]) - fun deleteRecord(@PathVariable cfid: String?) { - recordService!!.deleteRecord(cfid!!) + fun deleteRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String) { + recordService.deleteRecord(zone_id, dns_record_id) } - private fun Record.toDTO() = RecordResponseDTO( - cfid = cfid!!, - name = name!!, - type = type!!, - content = content!!, - prio = prio!!, - ttl = ttl!!, - changeDate = changeDate!!, - auth = auth, - disabled = disabled + @PatchMapping("{zone_id}/dns_records/{dns_record_id}") + @Operation(summary = "Update Record by ID", tags=["record"]) + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Return Record", + content = [Content(schema = Schema(implementation = RecordResponseDTO::class))]), + ApiResponse(responseCode = "400", description = "Bad request", + content = [Content(schema = Schema(implementation = Void::class))]), + ]) + fun updateRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String, @RequestBody record: RecordRequestDTO): RecordResponseDTO { + return recordService.updateRecord(zone_id, dns_record_id, record) + } + + private fun DomainRecord.toDTO() = RecordResponseDTO( + id = cfid, + type = type, + name = name, + content = content, + zoneId = domain.cfid, + zoneName = domain.name, + priority = prio, + ttl = ttl, + createdOn = createdOn.getISOFormat(), + modifiedOn = modifiedOn.getISOFormat(), + comment = comment ) } \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/db/Domain.kt b/src/main/kotlin/space/mori/dnsapi/db/Domain.kt index cb62324..f2abc5f 100644 --- a/src/main/kotlin/space/mori/dnsapi/db/Domain.kt +++ b/src/main/kotlin/space/mori/dnsapi/db/Domain.kt @@ -7,37 +7,17 @@ import java.util.* @Entity @Table(name = "domains") -class Domain { +data class Domain( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private val id: Long? = null + var id: Long? = null, - @Column(nullable = false, length = 255) - val domainName: String? = null + @Column(nullable = false, unique = true) + var name: String, - @Column(nullable = true, length = 128) - val master: String? = null - - @Column(nullable = true, name = "last_check") - val lastCheck: Int? = null - - @Column(nullable = false, length = 6) - val type: String? = null - - @Column(nullable = true, name = "notified_serial") - val notifiedSerial: Int? = null - - @Column(nullable = false, length = 128) - val account: String? = null - - @Column(unique = true, nullable = false, length = 32) - var cfid: String? = null - - @OneToMany(mappedBy = "domain", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) - private val records: Set? = null - - @PrePersist - protected fun onCreate() { - this.cfid = UUID.randomUUID().toString().replace("-", "") - } // Getters and setters + @Column(nullable = false, unique = true) + var cfid: String = UUID.randomUUID().toString().replace("-", "") +) { + @OneToMany(mappedBy = "domain", cascade = [CascadeType.ALL], orphanRemoval = true) + var records: List = mutableListOf() } \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/db/Record.kt b/src/main/kotlin/space/mori/dnsapi/db/Record.kt index 9b92264..8f2f08f 100644 --- a/src/main/kotlin/space/mori/dnsapi/db/Record.kt +++ b/src/main/kotlin/space/mori/dnsapi/db/Record.kt @@ -6,68 +6,28 @@ import java.util.* @Entity @Table(name = "records") -class Record { +data class Record( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private val id: Long? = null + var id: Long? = null, - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "domain_id", nullable = false) - var domain: Domain? = null + var domain: Domain, - @Column(nullable = false, length = 255) - var name: String? = null + var name: String, + var type: String, + var content: String, + var ttl: Int, + var prio: Int, + var disabled: Boolean, + var auth: Boolean, - @Column(nullable = false, length = 10) - var type: String? = null + var createdOn: Date, + var modifiedOn: Date, - @Column(nullable = false, length = 64000) - var content: String? = null + var comment: String, - @Column(nullable = true) - var ttl: Int? - - @Column(nullable = true) - var prio: Int? - - @Column(nullable = true) - var changeDate: Int? - - var disabled: Boolean = false - var auth: Boolean = true - - @Column(unique = true, nullable = false, length = 32) - var cfid: String? = null - - @Column(nullable = true, length = 64) - var comment: String? = null - - @PrePersist - private fun onCreate() { - this.cfid = UUID.randomUUID().toString().replace("-", "") - } // Getters and setters - - constructor( - name: String, - type: String, - content: String, - changeDate: Int?, - disabled: Boolean, - domain: Domain, - comment: String?, - auth: Boolean = true, - ttl: Int? = 300, - prio: Int? = 0, - ) { - this.name = name - this.type = type - this.content = content - this.ttl = ttl - this.prio = prio - this.changeDate = changeDate - this.disabled = disabled - this.auth = auth - this.domain = domain - this.comment = comment - } -} \ No newline at end of file + @Column(nullable = false, unique = true) + var cfid: String = UUID.randomUUID().toString().replace("-", "") +) \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/db/RecordRepository.kt b/src/main/kotlin/space/mori/dnsapi/db/RecordRepository.kt index 2430364..4268f9f 100644 --- a/src/main/kotlin/space/mori/dnsapi/db/RecordRepository.kt +++ b/src/main/kotlin/space/mori/dnsapi/db/RecordRepository.kt @@ -12,8 +12,11 @@ interface RecordRepository : JpaRepository { fun findByCfid(cfid: String): Optional @Transactional - fun deleteByCfid(cfid: String) + fun findByDomainIdAndCfid(domainId: Long, cfid: String): Optional - @Query("SELECT r FROM Record r WHERE r.domain.cfid = :domainCfid") - fun findByDomainCfid(@Param("domainCfid") cfid: String): List + @Transactional + fun deleteByCfid(cfid: String): Int + + @Transactional + fun deleteByDomainIdAndCfid(domain_id: Long, cfid: String): Int } \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/dto/DomainRequestDTO.kt b/src/main/kotlin/space/mori/dnsapi/dto/DomainRequestDTO.kt index a12144b..0a96943 100644 --- a/src/main/kotlin/space/mori/dnsapi/dto/DomainRequestDTO.kt +++ b/src/main/kotlin/space/mori/dnsapi/dto/DomainRequestDTO.kt @@ -5,5 +5,5 @@ import io.swagger.v3.oas.annotations.media.Schema @Schema(description = "Request DTO for Domain") data class DomainRequestDTO( @Schema(description = "Domain name(TLD)", example = "example.com") - val domainName: String + val name: String ) \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/dto/DomainResponseDTO.kt b/src/main/kotlin/space/mori/dnsapi/dto/DomainResponseDTO.kt index 3cfe534..915c280 100644 --- a/src/main/kotlin/space/mori/dnsapi/dto/DomainResponseDTO.kt +++ b/src/main/kotlin/space/mori/dnsapi/dto/DomainResponseDTO.kt @@ -5,8 +5,8 @@ import io.swagger.v3.oas.annotations.media.Schema @Schema(description = "Response DTO for Domain") data class DomainResponseDTO( @Schema(description = "Domain CFID", example = "123e4567e89b12d3a456426655440000") - val cfid: String, + val id: String, @Schema(description = "Domain name(TLD)", example = "example.com") - val domainName: String + val name: String ) \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/dto/RecordRequestDTO.kt b/src/main/kotlin/space/mori/dnsapi/dto/RecordRequestDTO.kt index 115e37d..91379b6 100644 --- a/src/main/kotlin/space/mori/dnsapi/dto/RecordRequestDTO.kt +++ b/src/main/kotlin/space/mori/dnsapi/dto/RecordRequestDTO.kt @@ -4,20 +4,23 @@ import io.swagger.v3.oas.annotations.media.Schema @Schema(description = "Request DTO for Record") data class RecordRequestDTO( - @Schema(description = "Host name", example = "www") - val host: String, - @Schema(description = "Record type", example = "A") val type: String, + @Schema(description = "Host name", example = "www.example.com.") + val name: String, + @Schema(description = "Record data", example = "192.0.2.1") - val data: String, + val content: String, @Schema(description = "TTL (Time to Live)", example = "3600") - val ttl: Int, + val ttl: Int = 300, - @Schema(description = "Domain CFID", example = "123e4567e89b12d3a456426655440000") - val cfid: String, + @Schema(description = "Priority", example = "0") + val priority: Int? = null, + + @Schema(description = "Proxied: cloudflare api compatibility", example = "false") + val proxied: Boolean = false, @Schema(description = "comment", example="") val comment: String diff --git a/src/main/kotlin/space/mori/dnsapi/dto/RecordResponseDTO.kt b/src/main/kotlin/space/mori/dnsapi/dto/RecordResponseDTO.kt index 77ea1ae..4f85bb4 100644 --- a/src/main/kotlin/space/mori/dnsapi/dto/RecordResponseDTO.kt +++ b/src/main/kotlin/space/mori/dnsapi/dto/RecordResponseDTO.kt @@ -5,30 +5,45 @@ import java.util.* @Schema(description = "Response DTO for Record") data class RecordResponseDTO( - @Schema(description = "Record CFID", example = "123e4567e89b12d3a456426655440001") - val cfid: String, - - @Schema(description = "Host name", example = "www.domain.tld") - val name: String, + @Schema(description = "Record ID", example = "123e4567e89b12d3a456426655440001") + val id: String, @Schema(description = "Record type", example = "A") - val type: String, + var type: String, - @Schema(description = "Record data", example = "192.0.2.1") - val content: String, + @Schema(description = "Record name", example = "test.example.com") + var name: String, - @Schema(description = "TTL (Time to Live)", example = "3600") - val ttl: Int, + @Schema(description = "Record content", example = "1.1.1.1") + var content: String, - @Schema(description = "TTL per second", example = "300s") - val prio: Int, + @Schema(description = "Zone(TLD) ID", example = "123e4567e89b12d3a456426655440001") + val zoneId: String, - @Schema(description = "Changed date with Unix Timestamp") - val changeDate: Int, + @Schema(description = "Zone name(TLD)", example = "example.com") + val zoneName: String, - @Schema(description = "is disabled?", example = "false") - val disabled: Boolean, + @Schema(description = "Record creation time", example = "2014-01-01T05:20:00.12345Z") + val createdOn: String, - @Schema(description = "is authed", example = "true") - val auth: Boolean, + @Schema(description = "Record modification time", example = "2014-01-01T05:20:00.12345Z") + val modifiedOn: String, + + @Schema(description = "Record priority", example = "0") + val priority: Int? = 0, + + @Schema(description = "is proxyable: must false", example = "false") + val proxiable: Boolean = false, + + @Schema(description = "is proxied: must false", example = "false") + val proxied: Boolean = false, + + @Schema(description = "Record TTL", example = "300") + val ttl: Int = 300, + + @Schema(description = "Record is locked: must false", example = "false") + val locked: Boolean = false, + + @Schema(description = "Record comments", example = "") + val comment: String? = null, ) \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/service/RecordService.kt b/src/main/kotlin/space/mori/dnsapi/service/RecordService.kt index fb7f669..bf6d385 100644 --- a/src/main/kotlin/space/mori/dnsapi/service/RecordService.kt +++ b/src/main/kotlin/space/mori/dnsapi/service/RecordService.kt @@ -2,45 +2,160 @@ package space.mori.dnsapi.service import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import space.mori.dnsapi.PowerDNSApiClient import space.mori.dnsapi.db.DomainRepository -import space.mori.dnsapi.db.RecordRepository import space.mori.dnsapi.db.Record as DomainRecord +import space.mori.dnsapi.db.RecordRepository +import space.mori.dnsapi.dto.DomainRequestDTO import space.mori.dnsapi.dto.RecordRequestDTO +import space.mori.dnsapi.dto.RecordResponseDTO +import space.mori.dnsapi.getISOFormat import java.util.* @Service -class RecordService { +class RecordService( @Autowired - private lateinit var domainRepository: DomainRepository + private val powerDNSApiClient: PowerDNSApiClient, @Autowired - private val recordRepository: RecordRepository? = null + private val domainRepository: DomainRepository, + @Autowired + private val recordRepository: RecordRepository +) { + fun createRecord(domain_id: String, recordRequest: RecordRequestDTO): RecordResponseDTO { + val domain = domainRepository.findByCfid(domain_id) + if(domain.isEmpty) throw RuntimeException("Failed to find domain in API: $domain_id") - fun getAllRecords(cfid: String): List { - return recordRepository!!.findByDomainCfid(cfid) - } - - fun getRecordById(cfid: String): Optional { - return recordRepository!!.findByCfid(cfid) - } - - fun createRecord(record: RecordRequestDTO): DomainRecord { - val domain = domainRepository.findByCfid(record.cfid) - .orElseThrow { IllegalArgumentException("Invalid domain CFID") } - val r = DomainRecord( - name = record.host, - type = record.type, - content = record.data, - ttl = record.ttl, - domain = domain, - comment = record.comment, - changeDate = java.util.Date().time.toInt(), - disabled = false + val response = powerDNSApiClient.createRecord(domain.get().name, recordRequest) + if (!response.statusCode.is2xxSuccessful) { + throw RuntimeException("Failed to create record in PowerDNS: ${response.body}") + } + val record = DomainRecord( + domain = domain.get(), + name = recordRequest.name, + type = recordRequest.type, + content = recordRequest.content, + ttl = recordRequest.ttl, + prio = recordRequest.priority ?: 0, + disabled = false, + auth = true, + createdOn = Date(), + modifiedOn = Date(), + comment = recordRequest.comment, + ) + + return RecordResponseDTO( + id = record.cfid, + type = record.type, + name = record.name, + content = record.content, + proxiable = false, + proxied = false, + ttl = record.ttl, + locked = false, + zoneId = record.cfid, + zoneName = domain.get().name, + createdOn = record.createdOn.getISOFormat(), + modifiedOn = record.modifiedOn.getISOFormat(), + priority = record.prio, + comment = record.comment ) - return recordRepository!!.save(r) } - fun deleteRecord(cfid: String) { - recordRepository!!.deleteByCfid(cfid) + fun getRecord(domain_id: String, record_id: String): RecordResponseDTO { + val domain = domainRepository.findByCfid(domain_id) + if(domain.isEmpty) throw RuntimeException("Failed to find domain in API: $domain_id") + + val record = domain.get().records.find { it.cfid == record_id } + if(record == null) throw RuntimeException("Failed to find record in API: $record_id") + + return RecordResponseDTO( + id = record.cfid, + type = record.type, + name = record.name, + content = record.content, + ttl = record.ttl, + zoneId = record.domain.cfid, + zoneName = record.domain.name, + createdOn = record.createdOn.getISOFormat(), + modifiedOn = record.modifiedOn.getISOFormat(), + comment = record.comment, + ) + } + + fun getRecordsByDomain(domain_id: String): List? { + val domain = domainRepository.findByCfid(domain_id).orElseThrow { RuntimeException("Failed to find domain in API: $domain_id") } + return domain?.records?.map { RecordResponseDTO( + id = it.cfid, + type = it.type, + name = it.name, + content = it.content, + zoneId = it.domain.cfid, + zoneName = it.domain.name, + priority = it.prio, + ttl = it.ttl, + createdOn = it.createdOn.getISOFormat(), + modifiedOn = it.modifiedOn.getISOFormat(), + comment = it.comment, + )} + } + + @Transactional + fun updateRecord(domainId: String, cfid: String, updatedRecord: RecordRequestDTO): RecordResponseDTO { + // 도메인 조회 + val domain = domainRepository.findByCfid(domainId) + .orElseThrow { RuntimeException("Domain not found") } + + // 레코드 조회 + val record = recordRepository.findByDomainIdAndCfid(domain.id!!, cfid) + .orElseThrow { RuntimeException("Record not found") } + + // 레코드 업데이트 + record.name = updatedRecord.name + record.type = updatedRecord.type + record.content = updatedRecord.content + record.ttl = updatedRecord.ttl + record.prio = updatedRecord.priority ?: 0 + record.comment = updatedRecord.comment + record.modifiedOn = Date() + + val response = powerDNSApiClient.createRecord(domain!!.name, updatedRecord) + if (!response.statusCode.is2xxSuccessful) { + throw RuntimeException("Failed to update record in PowerDNS: ${response.body}") + } + + // 저장 + val savedRecord = recordRepository.save(record) + return RecordResponseDTO( + id = savedRecord.cfid, + type = savedRecord.type, + name = savedRecord.name, + content = savedRecord.content, + proxiable = true, + proxied = false, + ttl = savedRecord.ttl, + locked = false, + zoneId = domain.cfid, + zoneName = domain.name, + createdOn = savedRecord.createdOn.getISOFormat(), + modifiedOn = savedRecord.modifiedOn.getISOFormat(), + priority = savedRecord.prio + ) + } + + fun deleteRecord(domain_id: String, record_id: String) { + val domain = domainRepository.findByCfid(domain_id).orElseThrow { RuntimeException("Failed to find domain in API: $domain_id") } + + val deletedCount = recordRepository.deleteByDomainIdAndCfid(domain.id!!, record_id) + + if(deletedCount == 0) throw RuntimeException("Failed to find record in API: $record_id") + } + + fun deleteDomain(name: String) { + val response = powerDNSApiClient.deleteDomain(name) + if (!response.statusCode.is2xxSuccessful) { + throw RuntimeException("Failed to delete domain in PowerDNS: ${response.body}") + } } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 24b7cd4..3241149 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -11,4 +11,6 @@ springdoc.swagger-ui.enabled=false springdoc.api-docs.path=/api-docs springdoc.default-consumes-media-type= application/json springdoc.default-produces-media-type= application/json -springdoc.version= '@project.version@' \ No newline at end of file +springdoc.version= '@project.version@' +pdns.api.key=${PDNS_API_KEY} +pdns.api.url=${PDNS_API_URL} \ No newline at end of file