<file_sep>package br.com.alura.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder


@Configuration
@EnableWebSecurity
class SecurityConfiguration(
    private val userDetailsService: UserDetailsService
) : WebSecurityConfigurerAdapter() {

    @Throws(Exception::class)
    override fun configure(auth: AuthenticationManagerBuilder) {
        auth.userDetailsService(this.userDetailsService)
            .passwordEncoder(bCryptPasswordEncoder())
    }

    override fun configure(http: HttpSecurity) {
        http
            .authorizeRequests()
            .antMatchers("/topics").hasAnyAuthority("WRITER_READER")
            .anyRequest()
            .authenticated()
            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .formLogin().disable()
            .httpBasic()
    }

    @Bean
    fun bCryptPasswordEncoder(): BCryptPasswordEncoder {
        return BCryptPasswordEncoder()
    }
}<file_sep>package br.com.alura.controller

import br.com.alura.dto.NewTopicForm
import br.com.alura.dto.TopicByCategoryDto
import br.com.alura.dto.TopicView
import br.com.alura.dto.UpdateTopicForm
import br.com.alura.model.Topic
import br.com.alura.service.TopicService
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.util.UriComponentsBuilder
import javax.transaction.Transactional
import javax.validation.Valid

@RestController
@RequestMapping("topics")
class TopicController(
    val service: TopicService
) {

    @GetMapping
    @Cacheable("topics")
    fun list(page: Pageable): Page<TopicView> {
        return service.getList(page)
    }

    @GetMapping("/report")
    fun report(): List<TopicByCategoryDto> {
        return service.report()
    }

    @GetMapping("/{id}")
    fun getById(@PathVariable id: Long): TopicView {
        return service.getById(id)
    }

    @PostMapping
    @Transactional
    @CacheEvict(value = ["topics"], allEntries = true)
    fun create(@RequestBody @Valid form: NewTopicForm, uriBuilder: UriComponentsBuilder): ResponseEntity<TopicView> {
        val topicView = service.save(form)
        val uri = uriBuilder.path("topics/${topicView.id}").build().toUri()
        return ResponseEntity.created(uri).body(topicView)
    }

    @PutMapping
    @Transactional
    @CacheEvict(value = ["topics"], allEntries = true)
    fun update(@RequestBody @Valid form: UpdateTopicForm): TopicView {
        return service.update(form)
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    @Transactional
    @CacheEvict(value = ["topics"], allEntries = true)
    fun delete(@PathVariable id: Long) {
        service.delete(id)
    }
}
<file_sep>package br.com.alura.repository

import br.com.alura.model.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository: JpaRepository<User, Long> {
    fun findByEmail(email: String): User?
}<file_sep>create table role
(
    id   bigint      not null auto_increment,
    name varchar(50) not null,
    primary key (id)
);

insert into role(id, name) values (1, 'WRITER_READER');
<file_sep>create table topic
(
    id        bigint       not null auto_increment,
    title     varchar(50)  not null,
    message   varchar(300) not null,
    created_at datetime     not null,
    status    varchar(20)  not null,
    course_id bigint       not null,
    author_id   bigint       not null,
    primary key (id),
    foreign key (course_id) references course (id),
    foreign key (author_id) references user (id)
);

<file_sep>package br.com.alura.model

import java.time.LocalDateTime
import javax.persistence.*

@Entity
data class TopicResponse(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val message: String,
    val createdAt: LocalDateTime = LocalDateTime.now(),
    @ManyToOne
    val author: User,
    @ManyToOne
    val topic: Topic,
    val solution: Boolean
)
<file_sep>create table course
(
    id       bigint      not null auto_increment,
    name     varchar(50) not null,
    category varchar(50) not null,
    primary key (id)
);

insert into course(id, name, category) values (1, 'kotlin', 'programing');
insert into course(id, name, category) values (2, 'react', 'front-end');<file_sep>update user set password='<PASSWORD>' where id=1;
update user set password='<PASSWORD>' where id=2;<file_sep>package br.com.alura.model

import java.time.LocalDateTime
import javax.persistence.*

@Entity
data class Topic(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val title: String,
    val message: String,
    val createdAt: LocalDateTime = LocalDateTime.now(),
    @ManyToOne
    val course: Course,
    @ManyToOne
    val author: User,
    @Enumerated(value = EnumType.STRING)
    val status: TopicStatus = TopicStatus.NO_ANSWERED,
    @OneToMany(mappedBy = "topic")
    val responses: List<TopicResponse> = ArrayList()
)
<file_sep>package br.com.alura.model

import javax.persistence.*

@Entity
@Table(name = "course")
data class Course(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val name: String,
    val category: String
)
<file_sep>package br.com.alura.repository

import br.com.alura.dto.TopicByCategoryDto
import br.com.alura.model.Topic
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface TopicRepository: JpaRepository<Topic, Long> {
    @Query("SELECT new br.com.alura.dto.TopicByCategoryDto(t.course.category, count(t)) from Topic t JOIN t.course GROUP BY t.course.category")
    fun report(): List<TopicByCategoryDto>
}<file_sep>package br.com.alura.repository

import br.com.alura.model.Course
import org.springframework.data.jpa.repository.JpaRepository

interface CourseRepository: JpaRepository<Course, Long> {
}<file_sep>package br.com.alura.service

import br.com.alura.model.User
import org.springframework.security.core.userdetails.UserDetails

class UserDetail(private val user: User) : UserDetails {
    override fun getAuthorities() = user.role

    override fun getPassword() = <PASSWORD>

    override fun getUsername() = user.email

    override fun isAccountNonExpired() = true

    override fun isAccountNonLocked() = true

    override fun isCredentialsNonExpired() = true

    override fun isEnabled() = true
}<file_sep>create table topic_response
(
    id        bigint       not null auto_increment,
    message   varchar(300) not null,
    created_at datetime     not null,
    status    varchar(20)  not null,
    topic_id  bigint       not null,
    author_id bigint       not null,
    solution  bigint       not null,
    primary key (id),
    foreign key (topic_id) references topic (id),
    foreign key (author_id) references user (id)
);

<file_sep>create table user_role
(
    id      bigint not null auto_increment,
    user_id bigint not null,
    role_id bigint not null,
    primary key (id),
    foreign key (user_id) references user(id),
    foreign key (role_id) references role(id)
);

insert into user_role(id, user_id, role_id) values (1, 1, 1);
<file_sep>package br.com.alura.service

import br.com.alura.model.User
import br.com.alura.repository.UserRepository
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.stereotype.Service
import java.lang.RuntimeException

@Service
class UserService(private val repository: UserRepository)     : UserDetailsService
{
    fun getById(id: Long): User = repository.getById(id)

    override fun loadUserByUsername(email: String): UserDetails {
       val user = repository.findByEmail(email) ?: throw RuntimeException("user not found")
        return UserDetail(user)
    }
}
<file_sep>package br.com.alura.model

enum class TopicStatus {
    NO_ANSWERED,
    NO_SOLVED,
    SOLVED,
    CLOSED
}
<file_sep>package br.com.alura.mapper

import br.com.alura.dto.NewTopicForm
import br.com.alura.model.Topic
import br.com.alura.service.CourseService
import br.com.alura.service.UserService
import org.springframework.stereotype.Component

@Component
class TopicFormMapper(
    private val courseService: CourseService,
    private val userService: UserService,
) : Mapper<NewTopicForm, Topic> {
    override fun map(t: NewTopicForm): Topic = Topic(
        title = t.title,
        message = t.message,
        course = courseService.getById(t.courseId),
        author = userService.getById(t.authorId)
    )
}
<file_sep>package br.com.alura.dto

import javax.validation.constraints.*

class NewTopicForm(
    @field:NotBlank @field:Size(min = 5, max = 100) val title: String,
    @field:NotBlank val message: String,
    @field:Positive val courseId: Long, // not null not working here
    @field:Positive val authorId: Long
)
<file_sep>package br.com.alura.service

import br.com.alura.model.Course
import br.com.alura.repository.CourseRepository
import org.springframework.stereotype.Service
import java.util.*

@Service
class CourseService(private val repository: CourseRepository) {
    fun getById(id: Long): Course = repository.getById(id)
}
<file_sep>package br.com.alura.exception

import br.com.alura.dto.ErrorView
import org.springframework.http.HttpStatus
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice
import javax.servlet.http.HttpServletRequest

@RestControllerAdvice
class ExceptionHandler {

    @ExceptionHandler(NotFoundException::class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    fun handleNotFound(exp: NotFoundException, request: HttpServletRequest): ErrorView {
        return ErrorView(
            status = HttpStatus.NOT_FOUND.value(),
            error = HttpStatus.NOT_FOUND.name,
            message = exp.message,
            path = request.servletPath
        )
    }

    @ExceptionHandler(MethodArgumentNotValidException::class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun handleValidationError(exp: MethodArgumentNotValidException, request: HttpServletRequest): ErrorView {
        val errorMessage = HashMap<String, String?>()
        exp.bindingResult.fieldErrors.forEach { e -> errorMessage[e.field] = e.defaultMessage }
        return ErrorView(
            status = HttpStatus.BAD_REQUEST.value(),
            error = HttpStatus.BAD_REQUEST.name,
            message = errorMessage.toString(),
            path = request.servletPath
        )
    }

    @ExceptionHandler(Exception::class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    fun handleServerError(exp: Exception, request: HttpServletRequest): ErrorView {
        return ErrorView(
            status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
            error = HttpStatus.INTERNAL_SERVER_ERROR.name,
            message = exp.message,
            path = request.servletPath
        )
    }
}
<file_sep>package br.com.alura.mapper

import br.com.alura.dto.TopicView
import br.com.alura.model.Topic
import org.springframework.stereotype.Component

@Component
class TopicViewMapper : Mapper<Topic, TopicView> {
    override fun map(t: Topic): TopicView {
        return TopicView(
            id = t.id,
            title = t.title,
            message = t.message,
            status = t.status,
            createdAt = t.createdAt
        )
    }
}
<file_sep>package br.com.alura.model

import com.fasterxml.jackson.annotation.JsonIgnore
import javax.persistence.*

@Entity
@Table(name = "user")
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val name: String,
    val email: String,
    val password: String,
    @JsonIgnore
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_role")
    val role: List<Role> = mutableListOf()
)
<file_sep>alter table user add column password text;<file_sep>package br.com.alura.service

import br.com.alura.dto.NewTopicForm
import br.com.alura.dto.TopicByCategoryDto
import br.com.alura.dto.TopicView
import br.com.alura.dto.UpdateTopicForm
import br.com.alura.exception.NotFoundException
import br.com.alura.mapper.TopicFormMapper
import br.com.alura.mapper.TopicViewMapper
import br.com.alura.model.Topic
import br.com.alura.repository.TopicRepository
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service

@Service
class TopicService(
    private val repository: TopicRepository,
    private val topicViewMapper: TopicViewMapper,
    private val topicFormMapper: TopicFormMapper
) {
    private val notFoundExceptionMessage = "Topic not found"
    fun getList(page: Pageable): Page<TopicView> {
        return repository.findAll(page).map { topicViewMapper.map(it) }
    }

    fun getById(id: Long): TopicView {
        val topic = repository.findById(id).orElseThrow { NotFoundException(notFoundExceptionMessage) }
        return topicViewMapper.map(topic)
    }

    fun save(form: NewTopicForm): TopicView {
        val newTopic = topicFormMapper.map(form)
        repository.save(newTopic)
        return topicViewMapper.map(newTopic)
    }

    fun update(form: UpdateTopicForm): TopicView {
        val topic = repository.findById(form.id).orElseThrow { NotFoundException(notFoundExceptionMessage) }
        val updatedTopic = topic.copy(title = form.title, message = form.message)
        repository.save(updatedTopic)
        return topicViewMapper.map(updatedTopic)
    }

    fun delete(id: Long) {
        repository.deleteById(id)
    }

    fun report(): List<TopicByCategoryDto> {
        return repository.report()
    }
}
<file_sep>create table user
(
    id    bigint      not null auto_increment,
    name  varchar(50) not null,
    email varchar(50) not null,
    primary key (id)
);

insert into user(id, name, email) values (1, '<NAME>', '<EMAIL>');
insert into user(id, name, email) values (2, '<NAME>', '<EMAIL>');
<file_sep>package br.com.alura.exception

class NotFoundException(message: String? = null) : RuntimeException(message) {
}
<file_sep>package br.com.alura.model

import org.springframework.security.core.GrantedAuthority
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id

@Entity
data class Role(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private val id: Long,
    private val name: String
) : GrantedAuthority {
    override fun getAuthority(): String = name
}<file_sep># API-kotlin-spring

# Topics
Web
    - Implemtação de GET, POST, PUT e DELETE
    - DTOs para representar as informações de input/output da API
    - Classes Mappers para conversão de DTOs
    - Validações utilizando o Bean Validation
    - Princípios do modelo REST
    - Tratamento de exceptions na API usando global handler
Data
    - Spring Data JPA
    - Flyway como ferramenta de migrations
    - Paginação e ordenação nas consultas
    - Cache
    - Queries personalizadas nas interfaces repository


# Requirements
    - java 11
    - maven ^3.8.1 

# Technologies
    - kotlin
    - spring 2.5.5
        - starter-web
        - starter-validation
        - starter-devtools
        - starter-test
        - starter-data-jpa
        - starter-cache
    - h2
    - flyway

# RUN
```
./mvnw clean install -DskipTests
./mvnw clean spring-boot: run
```

<file_sep>package br.com.alura.dto

import javax.validation.constraints.NotBlank
import javax.validation.constraints.Positive
import javax.validation.constraints.Size

class UpdateTopicForm(
    @field:Positive val id: Long,
    @field:NotBlank @field:Size(min = 5, max = 100) val title: String,
    @field:NotBlank val message: String
)
<file_sep>package br.com.alura.dto

data class TopicByCategoryDto(
    val category: String,
    val qnt: Long
)
