diff --git a/build.gradle.kts b/build.gradle.kts index 9671f63..dcb87a2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") - // implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") diff --git a/inc.env b/inc.env index 3025dd8..90030c5 100644 --- a/inc.env +++ b/inc.env @@ -10,6 +10,7 @@ MARIADB_USER=pdns MARIADB_PASSWORD= PDNS_API_KEY= +PDNS_API_URL= PDNS_WEBSERVER_KEY= PDNS_VERSION_STRING= MASTER_PDNS_IP= diff --git a/src/main/kotlin/space/mori/dnsapi/DnsapiApplication.kt b/src/main/kotlin/space/mori/dnsapi/DnsapiApplication.kt index ad8e0bd..27d7ee1 100644 --- a/src/main/kotlin/space/mori/dnsapi/DnsapiApplication.kt +++ b/src/main/kotlin/space/mori/dnsapi/DnsapiApplication.kt @@ -3,6 +3,8 @@ package space.mori.dnsapi import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import io.github.cdimascio.dotenv.dotenv +import org.springframework.security.core.context.SecurityContextHolder +import space.mori.dnsapi.db.User import java.time.OffsetDateTime import java.time.ZoneOffset import java.time.format.DateTimeFormatter diff --git a/src/main/kotlin/space/mori/dnsapi/SecurityConfig.kt b/src/main/kotlin/space/mori/dnsapi/SecurityConfig.kt new file mode 100644 index 0000000..a191b95 --- /dev/null +++ b/src/main/kotlin/space/mori/dnsapi/SecurityConfig.kt @@ -0,0 +1,42 @@ +package space.mori.dnsapi + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import space.mori.dnsapi.filter.UserFilter + +@Configuration +@EnableWebSecurity +class SecurityConfig { + @Autowired + private val service: UserFilter? = null + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + return http + .csrf{ it.disable() } + .cors{ it.disable() } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authorizeHttpRequests { + it.requestMatchers("/zones/**").authenticated() + it.requestMatchers( + "/swagger-ui.html", + "/swagger-ui/**", + "/api-docs/**", + "/v3/api-docs/**", + "/v2/api-docs/**", + "/swagger-resources/**", + "/webjars/**" + ).permitAll() + } + .addFilterBefore(service, UsernamePasswordAuthenticationFilter::class.java) + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/SwaggerConfig.kt b/src/main/kotlin/space/mori/dnsapi/SwaggerConfig.kt index 429091c..28fb083 100644 --- a/src/main/kotlin/space/mori/dnsapi/SwaggerConfig.kt +++ b/src/main/kotlin/space/mori/dnsapi/SwaggerConfig.kt @@ -3,23 +3,28 @@ package space.mori.dnsapi import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class SwaggerConfig { + private val securitySchemeName = "api token" + @Bean fun openAPI(): OpenAPI { return OpenAPI() - .components(Components()) - .info(apiInfo()) - } - - private fun apiInfo(): Info { - return Info() - .title("Spring Boot REST API Specifications") - .description("Specification") - .version("1.0.0") + .info(Info().title("Cloudflare compatible PowerDNS API Server").version("v1.0.0")) + .addSecurityItem(SecurityRequirement().addList(securitySchemeName)) + .components(Components() + .addSecuritySchemes(securitySchemeName, + SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + ) + ) } } \ 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 a0609a3..4f03ce5 100644 --- a/src/main/kotlin/space/mori/dnsapi/controller/DomainController.kt +++ b/src/main/kotlin/space/mori/dnsapi/controller/DomainController.kt @@ -1,8 +1,6 @@ package space.mori.dnsapi.controller import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.Parameter -import io.swagger.v3.oas.annotations.media.ArraySchema import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse @@ -10,61 +8,66 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.* import space.mori.dnsapi.db.Domain +import space.mori.dnsapi.dto.ApiResponseDTO +import space.mori.dnsapi.dto.DeleteResponseWithId +import space.mori.dnsapi.dto.DomainRequestDTO import space.mori.dnsapi.dto.DomainResponseDTO +import space.mori.dnsapi.filter.getCurrentUser import space.mori.dnsapi.service.DomainService -import java.util.* @RestController -@RequestMapping("/domain") -class DomainController { +@RequestMapping("/zones") +class DomainController( @Autowired - private val domainService: DomainService? = null - - @get:GetMapping - @get:Operation(summary = "Get all domains", tags = ["domain"]) - @get:ApiResponse(responseCode = "200", description = "Returns all domains", - content = [Content(array = ArraySchema(schema = Schema(implementation = DomainResponseDTO::class)))]) - val allDomains: List - get() = domainService!!.getAllDomains().map { it?.toDTO() } + private val domainService: DomainService +) { + @GetMapping + @Operation(summary = "Get all domains", tags = ["domain"]) + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Returns all domains", useReturnTypeSchema = true), + ApiResponse(responseCode = "404", description = "Returns not found", + content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]) + ]) + fun allDomains(): ApiResponseDTO> { + return ApiResponseDTO(result = domainService.getAllDomains().map { it.toDTO() }) + } @Operation(summary = "Get domain", tags = ["domain"]) @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Returns domain", - content = [Content(schema = Schema(implementation = DomainResponseDTO::class))]), + ApiResponse(responseCode = "200", description = "Returns domain", useReturnTypeSchema = true), ApiResponse(responseCode = "404", description = "Returns not found", - content = [Content(schema = Schema(implementation = Void::class))]) + content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]) ]) @GetMapping("/{cfid}") fun getDomainByCfid( - @Parameter(description = "CFID", required = true) @PathVariable cfid: String? - ): Optional { - return domainService!!.getDomainById(cfid!!).map { it.toDTO() } + ): ApiResponseDTO { + return ApiResponseDTO(result = domainService.getDomainById(cfid!!).toDTO()) } @Operation(summary = "Create domain", tags = ["domain"]) @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Created domain", - content = [Content(schema = Schema(implementation = DomainResponseDTO::class))]), + ApiResponse(responseCode = "200", description = "Created domain", useReturnTypeSchema = true), ApiResponse(responseCode = "400", description = "Bad request", - content = [Content(schema = Schema(implementation = Void::class))]) + content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]) ]) @PostMapping - fun createDomain(@RequestBody domain: Domain?): DomainResponseDTO { - return domainService!!.createDomain(domain!!).toDTO() + fun createDomain(@RequestBody domain: DomainRequestDTO): ApiResponseDTO { + return ApiResponseDTO(result = domainService.createDomain(domain).toDTO()) } @Operation(summary = "Delete domain", tags = ["domain"]) @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Deleted domain", - content = [Content(schema = Schema(implementation = Void::class))]), + ApiResponse(responseCode = "200", description = "Deleted domain", useReturnTypeSchema = true), ApiResponse(responseCode = "400", description = "Bad request", - content = [Content(schema = Schema(implementation = Void::class))]) + content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]) ]) - @DeleteMapping("/{cfid}") - fun deleteDomain(@PathVariable cfid: String?) { - domainService!!.deleteDomain(cfid!!) + @DeleteMapping("/{domain_id}") + fun deleteDomain(@PathVariable domain_id: String?): ApiResponseDTO { + domainService.deleteDomain(domain_id!!) + + 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 4a72387..a0b06f8 100644 --- a/src/main/kotlin/space/mori/dnsapi/controller/RecordController.kt +++ b/src/main/kotlin/space/mori/dnsapi/controller/RecordController.kt @@ -1,21 +1,16 @@ package space.mori.dnsapi.controller import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.media.ArraySchema import io.swagger.v3.oas.annotations.media.Content 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.db.Domain +import space.mori.dnsapi.dto.* 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("/zones") @@ -26,61 +21,57 @@ class RecordController( @GetMapping("{zone_id}/dns_records") @Operation(summary = "Get all records", tags=["record"]) @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Return All Records", - content = [Content(array = ArraySchema(schema = Schema(implementation = RecordResponseDTO::class)))]), + ApiResponse(responseCode = "200", description = "Return All Records", useReturnTypeSchema = true), ApiResponse(responseCode = "400", description = "Bad request", - content = [Content(schema = Schema(implementation = Void::class))]), + content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) - fun allRecords(@PathVariable zone_id: String): List { - return recordService.getRecordsByDomain(zone_id)?.map{ it } ?: listOf() + fun allRecords(@PathVariable zone_id: String): ApiResponseDTO> { + return ApiResponseDTO(result = recordService.getRecordsByDomain(zone_id)?.map{ it } ?: listOf()) } @GetMapping("{zone_id}/dns_records/{dns_record_id}") @Operation(summary = "Get Record by ID", tags=["record"]) @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Return Record", - content = [Content(schema = Schema(implementation = RecordResponseDTO::class))]), + ApiResponse(responseCode = "200", description = "Return Record", useReturnTypeSchema = true), ApiResponse(responseCode = "400", description = "Bad request", - content = [Content(schema = Schema(implementation = Void::class))]) + content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) - fun getRecordByCfid(@PathVariable zone_id: String, @PathVariable dns_record_id: String): RecordResponseDTO { - return recordService.getRecord(zone_id, dns_record_id) + fun getRecordByCfid(@PathVariable zone_id: String, @PathVariable dns_record_id: String): ApiResponseDTO { + return ApiResponseDTO(result = recordService.getRecord(zone_id, dns_record_id)) } @PostMapping("{zone_id}/dns_records") @Operation(summary = "Add Record by ID", tags=["record"]) @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Return Record", - content = [Content(schema = Schema(implementation = RecordResponseDTO::class))]), + ApiResponse(responseCode = "200", description = "Return Record", useReturnTypeSchema = true), ApiResponse(responseCode = "400", description = "Bad request", - content = [Content(schema = Schema(implementation = Void::class))]), + content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) - fun createRecord(@PathVariable zone_id: String, @RequestBody record: RecordRequestDTO): RecordResponseDTO { - return recordService.createRecord(zone_id, record) + fun createRecord(@PathVariable zone_id: String, @RequestBody record: RecordRequestDTO): ApiResponseDTO { + return ApiResponseDTO(result = recordService.createRecord(zone_id, record)) } @DeleteMapping("{zone_id}/dns_records/{dns_record_id}") @Operation(summary = "Remove Record by ID", tags=["record"]) @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Return Record", - content = [Content(schema = Schema(implementation = Void::class))]), + ApiResponse(responseCode = "200", description = "Return Record", useReturnTypeSchema = true), ApiResponse(responseCode = "400", description = "Bad request", - content = [Content(schema = Schema(implementation = Void::class))]), + content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]), ]) - fun deleteRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String) { - recordService.deleteRecord(zone_id, dns_record_id) + fun deleteRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String): ApiResponseDTO { + val record_id = recordService.deleteRecord(zone_id, dns_record_id) + return ApiResponseDTO(result = DeleteResponseWithId(record_id)) } @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 = "200", description = "Return Record", useReturnTypeSchema = true), ApiResponse(responseCode = "400", description = "Bad request", - content = [Content(schema = Schema(implementation = Void::class))]), + content = [Content(schema = Schema(implementation = ApiResponseDTO::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) + fun updateRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String, @RequestBody record: RecordRequestDTO): ApiResponseDTO { + return ApiResponseDTO(result = recordService.updateRecord(zone_id, dns_record_id, record)) } private fun DomainRecord.toDTO() = RecordResponseDTO( diff --git a/src/main/kotlin/space/mori/dnsapi/db/Domain.kt b/src/main/kotlin/space/mori/dnsapi/db/Domain.kt index f2abc5f..84f1cd3 100644 --- a/src/main/kotlin/space/mori/dnsapi/db/Domain.kt +++ b/src/main/kotlin/space/mori/dnsapi/db/Domain.kt @@ -1,6 +1,5 @@ package space.mori.dnsapi.db -import io.swagger.v3.oas.annotations.media.Schema import jakarta.persistence.* import java.util.* @@ -16,8 +15,14 @@ data class Domain( var name: String, @Column(nullable = false, unique = true) - var cfid: String = UUID.randomUUID().toString().replace("-", "") -) { - @OneToMany(mappedBy = "domain", cascade = [CascadeType.ALL], orphanRemoval = true) + var cfid: String = UUID.randomUUID().toString().replace("-", ""), + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + var user: User, + + @OneToMany(mappedBy = "domain", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY) var records: List = mutableListOf() +) { + override fun toString(): String = "Domain(id=$cfid, name='$name')" } \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/db/DomainRepository.kt b/src/main/kotlin/space/mori/dnsapi/db/DomainRepository.kt index 3801994..5f53360 100644 --- a/src/main/kotlin/space/mori/dnsapi/db/DomainRepository.kt +++ b/src/main/kotlin/space/mori/dnsapi/db/DomainRepository.kt @@ -10,5 +10,8 @@ interface DomainRepository : JpaRepository { fun findByCfid(cfid: String): Optional @Transactional - fun deleteByCfid(cfid: String) + fun findAllByUser(user: User): List + + @Transactional + fun deleteByCfid(cfid: String): Int } \ 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 4268f9f..012c705 100644 --- a/src/main/kotlin/space/mori/dnsapi/db/RecordRepository.kt +++ b/src/main/kotlin/space/mori/dnsapi/db/RecordRepository.kt @@ -2,8 +2,6 @@ package space.mori.dnsapi.db import jakarta.transaction.Transactional import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Query -import org.springframework.data.repository.query.Param import java.util.* diff --git a/src/main/kotlin/space/mori/dnsapi/db/User.kt b/src/main/kotlin/space/mori/dnsapi/db/User.kt new file mode 100644 index 0000000..d2f3a10 --- /dev/null +++ b/src/main/kotlin/space/mori/dnsapi/db/User.kt @@ -0,0 +1,22 @@ +package space.mori.dnsapi.db + +import jakarta.persistence.* +import org.springframework.security.core.context.SecurityContextHolder + +@Entity +data class User( + @Id + @GeneratedValue(strategy= GenerationType.IDENTITY) + val id: Long? = null, + + @Column(nullable = false, unique = true, name = "api_key", length = 64) + val apiKey: String, + + @Column(nullable = false, unique = false, length = 20) + val name: String, + + @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER) + val domains: List = mutableListOf(), +) { + override fun toString(): String = "User(id=$id, name='$name')" +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/db/UserRepository.kt b/src/main/kotlin/space/mori/dnsapi/db/UserRepository.kt new file mode 100644 index 0000000..bd5155f --- /dev/null +++ b/src/main/kotlin/space/mori/dnsapi/db/UserRepository.kt @@ -0,0 +1,7 @@ +package space.mori.dnsapi.db + +import org.springframework.data.jpa.repository.JpaRepository + +interface UserRepository: JpaRepository { + fun findByApiKey(apiKey: String): User? +} \ 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 new file mode 100644 index 0000000..ce97312 --- /dev/null +++ b/src/main/kotlin/space/mori/dnsapi/dto/ApiResponseDTO.kt @@ -0,0 +1,17 @@ +package space.mori.dnsapi.dto + +data class ApiResponseDTO( + val success: Boolean = true, + val errors: List = listOf(), + val messages: List = listOf(), + val result: T? = null +) + +data class ErrorOrMessage( + val code: Int, + val message: String +) + +data class DeleteResponseWithId( + val id: 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 91379b6..8bb37a3 100644 --- a/src/main/kotlin/space/mori/dnsapi/dto/RecordRequestDTO.kt +++ b/src/main/kotlin/space/mori/dnsapi/dto/RecordRequestDTO.kt @@ -7,7 +7,7 @@ data class RecordRequestDTO( @Schema(description = "Record type", example = "A") val type: String, - @Schema(description = "Host name", example = "www.example.com.") + @Schema(description = "Host name", example = "www") val name: String, @Schema(description = "Record data", example = "192.0.2.1") diff --git a/src/main/kotlin/space/mori/dnsapi/dto/RecordResponseDTO.kt b/src/main/kotlin/space/mori/dnsapi/dto/RecordResponseDTO.kt index 4f85bb4..13ac936 100644 --- a/src/main/kotlin/space/mori/dnsapi/dto/RecordResponseDTO.kt +++ b/src/main/kotlin/space/mori/dnsapi/dto/RecordResponseDTO.kt @@ -11,7 +11,7 @@ data class RecordResponseDTO( @Schema(description = "Record type", example = "A") var type: String, - @Schema(description = "Record name", example = "test.example.com") + @Schema(description = "Record name", example = "test") var name: String, @Schema(description = "Record content", example = "1.1.1.1") diff --git a/src/main/kotlin/space/mori/dnsapi/filter/UserFilter.kt b/src/main/kotlin/space/mori/dnsapi/filter/UserFilter.kt new file mode 100644 index 0000000..dd3b1ab --- /dev/null +++ b/src/main/kotlin/space/mori/dnsapi/filter/UserFilter.kt @@ -0,0 +1,38 @@ +package space.mori.dnsapi.filter + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import space.mori.dnsapi.db.User +import space.mori.dnsapi.db.UserRepository + +@Component +class UserFilter( + @Autowired + private val userRepository: UserRepository +): OncePerRequestFilter() { + override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { + val apiKey = request.getHeader(HttpHeaders.AUTHORIZATION) + if(apiKey != null) { + val user = userRepository.findByApiKey(apiKey.replace("Bearer ", "")) + if(user != null) { + val authentication = UsernamePasswordAuthenticationToken( + user, null, emptyList() + ) + authentication.details = WebAuthenticationDetailsSource().buildDetails(request) + SecurityContextHolder.getContext().authentication = authentication + } + } + chain.doFilter(request, response) + } +} + +fun getCurrentUser(): User = + SecurityContextHolder.getContext().authentication.principal as User \ No newline at end of file diff --git a/src/main/kotlin/space/mori/dnsapi/service/DomainService.kt b/src/main/kotlin/space/mori/dnsapi/service/DomainService.kt index 0b6379f..0727f9f 100644 --- a/src/main/kotlin/space/mori/dnsapi/service/DomainService.kt +++ b/src/main/kotlin/space/mori/dnsapi/service/DomainService.kt @@ -2,29 +2,56 @@ package space.mori.dnsapi.service import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service +import space.mori.dnsapi.PowerDNSApiClient import space.mori.dnsapi.db.Domain import space.mori.dnsapi.db.DomainRepository +import space.mori.dnsapi.db.UserRepository +import space.mori.dnsapi.dto.DomainRequestDTO +import space.mori.dnsapi.filter.getCurrentUser import java.util.* @Service -class DomainService { +class DomainService( @Autowired - private val domainRepository: DomainRepository? = null + private val userRepository: UserRepository, + @Autowired + private val domainRepository: DomainRepository, + @Autowired + private val powerDNSApiClient: PowerDNSApiClient +) { + fun getAllDomains(): List { + val user = getCurrentUser() + val domain = domainRepository.findAllByUser(user) + if(domain.isEmpty()) throw RuntimeException("Unauthorized") - fun getAllDomains(): List { - return domainRepository!!.findAll() + return domain } - fun getDomainById(cfid: String): Optional { - return domainRepository!!.findByCfid(cfid) + 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") + + return domain } - fun createDomain(domain: Domain): Domain { - return domainRepository!!.save(domain) + fun createDomain(domain: DomainRequestDTO): Domain { + val user = getCurrentUser() + + powerDNSApiClient.createDomain(domain.name) + val saved_domain = domainRepository.save(Domain(name=domain.name, user=user)) + + return saved_domain } - fun deleteDomain(cfid: String) { - domainRepository!!.deleteByCfid(cfid) + fun deleteDomain(domain_id: String): String { + 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 diff --git a/src/main/kotlin/space/mori/dnsapi/service/RecordService.kt b/src/main/kotlin/space/mori/dnsapi/service/RecordService.kt index bf6d385..2aadd3a 100644 --- a/src/main/kotlin/space/mori/dnsapi/service/RecordService.kt +++ b/src/main/kotlin/space/mori/dnsapi/service/RecordService.kt @@ -7,9 +7,11 @@ import space.mori.dnsapi.PowerDNSApiClient import space.mori.dnsapi.db.DomainRepository import space.mori.dnsapi.db.Record as DomainRecord import space.mori.dnsapi.db.RecordRepository +import space.mori.dnsapi.db.UserRepository import space.mori.dnsapi.dto.DomainRequestDTO import space.mori.dnsapi.dto.RecordRequestDTO import space.mori.dnsapi.dto.RecordResponseDTO +import space.mori.dnsapi.filter.getCurrentUser import space.mori.dnsapi.getISOFormat import java.util.* @@ -21,18 +23,25 @@ class RecordService( @Autowired private val domainRepository: DomainRepository, @Autowired - private val recordRepository: RecordRepository + private val recordRepository: RecordRepository, + @Autowired + private val userRepository: UserRepository, ) { 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") + val domain = domainRepository.findByCfid(domain_id).orElseThrow { + throw RuntimeException("Failed to find domain in API: $domain_id") + } - val response = powerDNSApiClient.createRecord(domain.get().name, recordRequest) + val user = getCurrentUser() + if(domain.user.id != user.id) + throw RuntimeException("Unauthorized to create record in API: $domain_id") + + val response = powerDNSApiClient.createRecord(domain.name, recordRequest) if (!response.statusCode.is2xxSuccessful) { throw RuntimeException("Failed to create record in PowerDNS: ${response.body}") } val record = DomainRecord( - domain = domain.get(), + domain = domain, name = recordRequest.name, type = recordRequest.type, content = recordRequest.content, @@ -55,7 +64,7 @@ class RecordService( ttl = record.ttl, locked = false, zoneId = record.cfid, - zoneName = domain.get().name, + zoneName = domain.name, createdOn = record.createdOn.getISOFormat(), modifiedOn = record.modifiedOn.getISOFormat(), priority = record.prio, @@ -64,10 +73,15 @@ class RecordService( } 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 domain = domainRepository.findByCfid(domain_id).orElseThrow { + RuntimeException("Failed to find domain in API: $domain_id") + } - val record = domain.get().records.find { it.cfid == record_id } + val user = getCurrentUser() + if(domain.user.id != user.id) + throw RuntimeException("Unauthorized to get record in API: $domain_id") + + val record = domain.records.find { it.cfid == record_id } if(record == null) throw RuntimeException("Failed to find record in API: $record_id") return RecordResponseDTO( @@ -85,7 +99,14 @@ class RecordService( } fun getRecordsByDomain(domain_id: String): List? { - val domain = domainRepository.findByCfid(domain_id).orElseThrow { RuntimeException("Failed to find domain in API: $domain_id") } + 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?.records?.map { RecordResponseDTO( id = it.cfid, type = it.type, @@ -102,10 +123,15 @@ class RecordService( } @Transactional - fun updateRecord(domainId: String, cfid: String, updatedRecord: RecordRequestDTO): RecordResponseDTO { + fun updateRecord(domain_id: String, cfid: String, updatedRecord: RecordRequestDTO): RecordResponseDTO { // 도메인 조회 - val domain = domainRepository.findByCfid(domainId) - .orElseThrow { RuntimeException("Domain not found") } + 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") // 레코드 조회 val record = recordRepository.findByDomainIdAndCfid(domain.id!!, cfid) @@ -144,18 +170,18 @@ class RecordService( ) } - fun deleteRecord(domain_id: String, record_id: String) { - val domain = domainRepository.findByCfid(domain_id).orElseThrow { RuntimeException("Failed to find domain in API: $domain_id") } + fun deleteRecord(domain_id: String, record_id: String): String { + 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") 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}") - } + else return record_id } } \ No newline at end of file