, currentUser: User)
238 | = mapOf("articles" to articles.map { ArticleIO.fromModel(it, userService.currentUser()) },
239 | "articlesCount" to articles.size)
240 |
241 | fun commentView(comment: Comment, currentUser: User)
242 | = mapOf("comment" to CommentOut.fromModel(comment, currentUser))
243 |
244 | fun commentsView(comments: List, currentUser: User)
245 | = mapOf("comments" to comments.map { CommentOut.fromModel(it, currentUser) })
246 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/realworld/web/InvalidRequestHandler.kt:
--------------------------------------------------------------------------------
1 | package io.realworld.web
2 |
3 | import io.realworld.exception.InvalidException
4 | import org.springframework.http.HttpStatus
5 | import org.springframework.stereotype.Component
6 | import org.springframework.web.bind.annotation.ExceptionHandler
7 | import org.springframework.web.bind.annotation.ResponseBody
8 | import org.springframework.web.bind.annotation.ResponseStatus
9 | import org.springframework.web.bind.annotation.RestControllerAdvice
10 |
11 | /**
12 | * Generates an error with the following format:
13 | *
14 |
15 | {
16 | "errors":{
17 | "body": [
18 | "can't be empty"
19 | ]
20 | }
21 | }
22 |
23 | */
24 | @Component
25 | @RestControllerAdvice
26 | class InvalidRequestHandler {
27 | @ResponseBody
28 | @ExceptionHandler
29 | @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
30 | fun processValidationError(ex: InvalidException): Any {
31 | val errors = mutableMapOf>()
32 | ex.errors?.fieldErrors?.forEach {
33 | if (errors.containsKey(it.field))
34 | errors[it.field]!!.add(it.defaultMessage)
35 | else
36 | errors[it.field] = mutableListOf(it.defaultMessage)
37 | }
38 | return mapOf("errors" to errors)
39 | }
40 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/realworld/web/ProfileHandler.kt:
--------------------------------------------------------------------------------
1 | package io.realworld.web
2 |
3 | import io.realworld.exception.NotFoundException
4 | import io.realworld.jwt.ApiKeySecured
5 | import io.realworld.model.User
6 | import io.realworld.model.inout.Profile
7 | import io.realworld.repository.UserRepository
8 | import io.realworld.service.UserService
9 | import org.springframework.web.bind.annotation.*
10 |
11 | @RestController
12 | class ProfileHandler(val userRepository: UserRepository,
13 | val userService: UserService) {
14 |
15 | @ApiKeySecured(mandatory = false)
16 | @GetMapping("/api/profiles/{username}")
17 | fun profile(@PathVariable username: String): Any {
18 | userRepository.findByUsername(username)?.let {
19 | return view(it, userService.currentUser())
20 | }
21 | throw NotFoundException()
22 | }
23 |
24 | @ApiKeySecured
25 | @PostMapping("/api/profiles/{username}/follow")
26 | fun follow(@PathVariable username: String): Any {
27 | userRepository.findByUsername(username)?.let {
28 | var currentUser = userService.currentUser()
29 | if (!currentUser.follows.contains(it)) {
30 | currentUser.follows.add(it)
31 | currentUser = userService.setCurrentUser(userRepository.save(currentUser))
32 | }
33 | return view(it, currentUser)
34 | }
35 | throw NotFoundException()
36 | }
37 |
38 | @ApiKeySecured
39 | @DeleteMapping("/api/profiles/{username}/follow")
40 | fun unfollow(@PathVariable username: String): Any {
41 | userRepository.findByUsername(username)?.let {
42 | var currentUser = userService.currentUser()
43 | if (currentUser.follows.contains(it)) {
44 | currentUser.follows.remove(it)
45 | currentUser = userService.setCurrentUser(userRepository.save(currentUser))
46 | }
47 | return view(it, currentUser)
48 | }
49 | throw NotFoundException()
50 | }
51 |
52 | fun view(user: User, currentUser: User) = mapOf("profile" to Profile.fromUser(user, currentUser))
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/realworld/web/TagHandler.kt:
--------------------------------------------------------------------------------
1 | package io.realworld.web
2 |
3 | import io.realworld.model.Tag
4 | import io.realworld.repository.TagRepository
5 | import org.springframework.web.bind.annotation.GetMapping
6 | import org.springframework.web.bind.annotation.RestController
7 |
8 | @RestController
9 | class TagHandler(val repository: TagRepository) {
10 | @GetMapping("/api/tags")
11 | fun allTags() = mapOf("tags" to repository.findAll().map(Tag::name))
12 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/realworld/web/UserHandler.kt:
--------------------------------------------------------------------------------
1 | package io.realworld.web
2 |
3 | import io.realworld.exception.ForbiddenRequestException
4 | import io.realworld.exception.InvalidException
5 | import io.realworld.exception.InvalidLoginException
6 | import io.realworld.exception.InvalidRequest
7 | import io.realworld.jwt.ApiKeySecured
8 | import io.realworld.model.User
9 | import io.realworld.model.inout.Login
10 | import io.realworld.model.inout.Register
11 | import io.realworld.model.inout.UpdateUser
12 | import io.realworld.repository.UserRepository
13 | import io.realworld.service.UserService
14 | import org.mindrot.jbcrypt.BCrypt
15 | import org.springframework.validation.BindException
16 | import org.springframework.validation.Errors
17 | import org.springframework.validation.FieldError
18 | import org.springframework.web.bind.annotation.*
19 | import jakarta.validation.Valid
20 |
21 | @RestController
22 | class UserHandler(val repository: UserRepository,
23 | val service: UserService) {
24 |
25 | @PostMapping("/api/users/login")
26 | fun login(@Valid @RequestBody login: Login, errors: Errors): Any {
27 | InvalidRequest.check(errors)
28 |
29 | try {
30 | service.login(login)?.let {
31 | return view(service.updateToken(it))
32 | }
33 | return ForbiddenRequestException()
34 | } catch (e: InvalidLoginException) {
35 | val loginErrors = BindException(this, "")
36 | loginErrors.addError(FieldError("", e.field, e.error))
37 | throw InvalidException(loginErrors)
38 | }
39 | }
40 |
41 | @PostMapping("/api/users")
42 | fun register(@Valid @RequestBody register: Register, errors: Errors): Any {
43 | InvalidRequest.check(errors)
44 |
45 | // check for duplicate user
46 | val registerErrors = BindException(this, "")
47 | checkUserAvailability(registerErrors, register.email, register.username)
48 | InvalidRequest.check(registerErrors)
49 |
50 | val user = User(username = register.username!!,
51 | email = register.email!!, password = BCrypt.hashpw(register.password, BCrypt.gensalt()))
52 | user.token = service.newToken(user)
53 |
54 | return view(repository.save(user))
55 | }
56 |
57 | @ApiKeySecured
58 | @GetMapping("/api/user")
59 | fun currentUser() = view(service.currentUser())
60 |
61 | @ApiKeySecured
62 | @PutMapping("/api/user")
63 | fun updateUser(@Valid @RequestBody user: UpdateUser, errors: Errors): Any {
64 | InvalidRequest.check(errors)
65 |
66 | val currentUser = service.currentUser()
67 |
68 | // check for errors
69 | val updateErrors = BindException(this, "")
70 | if (currentUser.email != user.email && user.email != null) {
71 | if (repository.existsByEmail(user.email!!)) {
72 | updateErrors.addError(FieldError("", "email", "already taken"))
73 | }
74 | }
75 | if (currentUser.username != user.username && user.username != null) {
76 | if (repository.existsByUsername(user.username!!)) {
77 | updateErrors.addError(FieldError("", "username", "already taken"))
78 | }
79 | }
80 | if (user.password == "") {
81 | updateErrors.addError(FieldError("", "password", "can't be empty"))
82 | }
83 | InvalidRequest.check(updateErrors)
84 |
85 | // update the user
86 | val u = currentUser.copy(email = user.email ?: currentUser.email, username = user.username ?: currentUser.username,
87 | password = BCrypt.hashpw(user.password, BCrypt.gensalt()), image = user.image ?: currentUser.image,
88 | bio = user.bio ?: currentUser.bio)
89 | // update token only if email changed
90 | if (currentUser.email != u.email) {
91 | u.token = service.newToken(u)
92 | }
93 |
94 | return view(repository.save(u))
95 | }
96 |
97 | private fun checkUserAvailability(errors: BindException, email: String?, username: String?) {
98 | email?.let {
99 | if (repository.existsByEmail(it)) {
100 | errors.addError(FieldError("", "email", "already taken"))
101 | }
102 | }
103 | username?.let {
104 | if (repository.existsByUsername(it)) {
105 | errors.addError(FieldError("", "username", "already taken"))
106 | }
107 | }
108 | }
109 |
110 | fun view(user: User) = mapOf("user" to user)
111 | }
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.datasource.url=jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;Mode=Oracle
2 | spring.datasource.driverClassName=org.h2.Driver
3 | spring.datasource.username=sa
4 | spring.datasource.password=
5 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
6 |
7 | spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true
8 |
9 | jwt.secret=Em3u7dCZ2QSvSGSGSRFUTfrwgu3WjfU2rHZxSjNSqU5x89C3jXPL6WLMW7dTE6rd9NRgWAwUWHkj8ZLfbCNU8uVfv9kuBmWCYPkk776A5jQ2LeJ76bZbdhXN
10 | jwt.issuer=Kotlin&Spring
11 |
12 | #logging.level.org.springframework.web=DEBUG
--------------------------------------------------------------------------------
/src/test/kotlin/io/realworld/ApiApplicationTests.kt:
--------------------------------------------------------------------------------
1 | package io.realworld
2 |
3 | import feign.Feign
4 | import feign.gson.GsonDecoder
5 | import feign.gson.GsonEncoder
6 | import io.realworld.client.ProfileClient
7 | import io.realworld.client.TagClient
8 | import io.realworld.client.UserClient
9 | import io.realworld.client.response.InLogin
10 | import io.realworld.client.response.InRegister
11 | import io.realworld.model.inout.Login
12 | import io.realworld.model.inout.Register
13 | import org.hamcrest.Matchers
14 | import org.junit.Assert
15 | import org.junit.Before
16 | import org.junit.Test
17 | import org.junit.runner.RunWith
18 | import org.springframework.beans.factory.annotation.Autowired
19 | import org.springframework.boot.test.context.SpringBootTest
20 | import org.springframework.core.env.Environment
21 | import org.springframework.test.context.junit4.SpringRunner
22 |
23 |
24 | @RunWith(SpringRunner::class)
25 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
26 | class ApiApplicationTests {
27 |
28 | var randomServerPort: Int = 0
29 | @Autowired
30 | var environment: Environment? = null
31 | var tagClient: TagClient? = null
32 | var userClient: UserClient? = null
33 | var profileClient: ProfileClient? = null
34 |
35 | fun buildClient(t: Class): T {
36 | environment.let {
37 | randomServerPort = Integer.valueOf(it!!.getProperty("local.server.port"))
38 | return Feign.builder()
39 | .encoder(GsonEncoder())
40 | .decoder(GsonDecoder())
41 | .target(t, "http://localhost:${randomServerPort}")
42 | }
43 | }
44 |
45 | @Before
46 | fun before() {
47 | tagClient = buildClient(TagClient::class.java)
48 | userClient = buildClient(UserClient::class.java)
49 | profileClient = buildClient(ProfileClient::class.java)
50 | }
51 |
52 | @Test
53 | fun retrieveTags() {
54 | println("> tags: " + tagClient?.tags()?.tags)
55 | }
56 |
57 | @Test
58 | fun userAndProfileTest() {
59 | val fooRegister = userClient?.register(
60 | InRegister(Register(username = "foo", email = "foo@foo.com", password = "foo")))
61 | Assert.assertEquals("foo", fooRegister?.user?.username)
62 | Assert.assertEquals("foo@foo.com", fooRegister?.user?.email)
63 | Assert.assertThat(fooRegister?.user?.token, Matchers.notNullValue())
64 | println("Register foo OK")
65 |
66 | val fooLogin = userClient?.login(InLogin(Login(email = "foo@foo.com", password = "foo")))
67 | Assert.assertEquals("foo", fooLogin?.user?.username)
68 | Assert.assertEquals("foo@foo.com", fooLogin?.user?.email)
69 | Assert.assertThat(fooLogin?.user?.token, Matchers.notNullValue())
70 | println("Login foo OK")
71 |
72 | val barRegister = userClient?.register(
73 | InRegister(Register(username = "bar", email = "bar@bar.com", password = "bar")))
74 | Assert.assertEquals("bar", barRegister?.user?.username)
75 | Assert.assertEquals("bar@bar.com", barRegister?.user?.email)
76 | Assert.assertThat(barRegister?.user?.token, Matchers.notNullValue())
77 | println("Register bar OK")
78 |
79 | val barLogin = userClient?.login(InLogin(Login(email = "bar@bar.com", password = "bar")))
80 | Assert.assertEquals("bar", barLogin?.user?.username)
81 | Assert.assertEquals("bar@bar.com", barLogin?.user?.email)
82 | Assert.assertThat(barLogin?.user?.token, Matchers.notNullValue())
83 | println("Login bar OK")
84 |
85 | var profile = profileClient?.profile(barLogin?.user?.token!!, "foo")?.profile
86 | Assert.assertEquals("foo", profile?.username)
87 | Assert.assertFalse(profile?.following!!)
88 | println("Profile foo requested by bar OK")
89 |
90 | profile = profileClient?.follow(barLogin?.user?.token!!, "foo")?.profile
91 | Assert.assertEquals("foo", profile?.username)
92 | Assert.assertTrue(profile?.following!!)
93 | println("Foo is followed by bar OK")
94 |
95 | profile = profileClient?.unfollow(barLogin?.user?.token!!, "foo")?.profile
96 | Assert.assertEquals("foo", profile?.username)
97 | Assert.assertFalse(profile?.following!!)
98 | println("Foo is unfollowed by bar OK")
99 | }
100 | }
101 |
--------------------------------------------------------------------------------