diff --git a/build.gradle.kts b/build.gradle.kts index 86c3f89..36883e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,4 +31,6 @@ dependencies { testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") implementation("org.litote.kmongo:kmongo:$kmongo_version") + implementation("io.ktor:ktor-server-auth-jvm:$ktor_version") + implementation("io.ktor:ktor-server-auth-jwt-jvm:$ktor_version") } \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/Application.kt b/src/main/kotlin/com/ray650128/Application.kt index 32bae90..ac2a800 100644 --- a/src/main/kotlin/com/ray650128/Application.kt +++ b/src/main/kotlin/com/ray650128/Application.kt @@ -1,12 +1,31 @@ package com.ray650128 +import com.ray650128.dto.UserDto +import com.ray650128.model.User import com.ray650128.plugins.configureRouting import com.ray650128.plugins.configureSerialization +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0") { + install(Authentication) { + jwt { + verifier(JwtConfig.verifier) + realm = JwtConfig.myRealm + validate { + val name = it.payload.getClaim("account").asString() + if (name != null) { + UserDto(account = name, password = "") + } else { + null + } + } + } + } configureRouting() configureSerialization() }.start(wait = true) diff --git a/src/main/kotlin/com/ray650128/JwtConfig.kt b/src/main/kotlin/com/ray650128/JwtConfig.kt new file mode 100644 index 0000000..77a4318 --- /dev/null +++ b/src/main/kotlin/com/ray650128/JwtConfig.kt @@ -0,0 +1,35 @@ +package com.ray650128 + +import com.auth0.jwt.JWT +import com.auth0.jwt.JWTVerifier +import com.auth0.jwt.algorithms.Algorithm +import com.ray650128.dto.UserDto +import java.util.* + +object JwtConfig { + val secret = "my-secret" + val issuer = "com.ray650128" + val myRealm = "com.ray650128" + private val validityInMs = 36_000_00 * 24 // 1 day + private val algorithm = Algorithm.HMAC512(secret) + + val verifier: JWTVerifier = JWT + .require(algorithm) + .withIssuer(issuer) + .build() + + /** + * Produce a token for this combination of name and password + */ + fun generateToken(user: UserDto): String = JWT.create() + .withSubject("Authentication") + .withIssuer(issuer) + .withClaim("account", user.account) + //.withExpiresAt(getExpiration()) // optional + .sign(algorithm) + + /** + * Calculate the expiration Date based on current time + the given validity + */ + private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs) +} \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/dto/PersonDto.kt b/src/main/kotlin/com/ray650128/dto/PersonDto.kt deleted file mode 100644 index f05fa32..0000000 --- a/src/main/kotlin/com/ray650128/dto/PersonDto.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ray650128.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class PersonDto( - val id: String? = null, - val name: String, - val age: Int -) \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/dto/UserDto.kt b/src/main/kotlin/com/ray650128/dto/UserDto.kt new file mode 100644 index 0000000..b15203d --- /dev/null +++ b/src/main/kotlin/com/ray650128/dto/UserDto.kt @@ -0,0 +1,15 @@ +package com.ray650128.dto + +import io.ktor.server.auth.* +import kotlinx.serialization.Serializable + +@Serializable +data class UserDto( + val id: String? = null, + val account: String, + val password: String, + val name: String? = null, + var token: String? = null, + var createAt: Long? = null, + var updatedAt: Long? = null +): Principal \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/extension/PersonExtension.kt b/src/main/kotlin/com/ray650128/extension/PersonExtension.kt deleted file mode 100644 index 439f428..0000000 --- a/src/main/kotlin/com/ray650128/extension/PersonExtension.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ray650128.extension - -import com.ray650128.dto.PersonDto -import com.ray650128.model.Person - -fun Person.toDto(): PersonDto = - PersonDto( - id = this.id.toString(), - name = this.name, - age = this.age - ) - -fun PersonDto.toPerson(): Person = - Person( - name = this.name, - age = this.age - ) \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/extension/UserExtension.kt b/src/main/kotlin/com/ray650128/extension/UserExtension.kt new file mode 100644 index 0000000..4ddead1 --- /dev/null +++ b/src/main/kotlin/com/ray650128/extension/UserExtension.kt @@ -0,0 +1,25 @@ +package com.ray650128.extension + +import com.ray650128.dto.UserDto +import com.ray650128.model.User + +fun User.toDto(): UserDto = + UserDto( + id = this.id.toString(), + account = this.account, + password = this.password, + name = this.name, + token = this.token, + createAt = this.createAt, + updatedAt = this.updatedAt + ) + +fun UserDto.toUser(): User = + User( + account = this.account, + password = this.password, + name = this.name, + token = this.token, + createAt = this.createAt, + updatedAt = this.updatedAt + ) \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/model/LoginResult.kt b/src/main/kotlin/com/ray650128/model/LoginResult.kt new file mode 100644 index 0000000..c78d64f --- /dev/null +++ b/src/main/kotlin/com/ray650128/model/LoginResult.kt @@ -0,0 +1,9 @@ +package com.ray650128.model + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginResult( + var account: String, + var token: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/model/Person.kt b/src/main/kotlin/com/ray650128/model/Person.kt deleted file mode 100644 index f7a362e..0000000 --- a/src/main/kotlin/com/ray650128/model/Person.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.ray650128.model - -import org.bson.codecs.pojo.annotations.BsonId -import org.litote.kmongo.Id - -data class Person( - @BsonId - val id: Id? = null, - val name: String, - val age: Int -) \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/model/User.kt b/src/main/kotlin/com/ray650128/model/User.kt new file mode 100644 index 0000000..74c87be --- /dev/null +++ b/src/main/kotlin/com/ray650128/model/User.kt @@ -0,0 +1,15 @@ +package com.ray650128.model + +import org.bson.codecs.pojo.annotations.BsonId +import org.litote.kmongo.Id + +data class User( + @BsonId + val id: Id? = null, + val account: String, + val password: String, + var name: String? = null, + var token: String? = null, + var createAt: Long? = null, + var updatedAt: Long? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/plugins/Routing.kt b/src/main/kotlin/com/ray650128/plugins/Routing.kt index aef9b1e..4565709 100644 --- a/src/main/kotlin/com/ray650128/plugins/Routing.kt +++ b/src/main/kotlin/com/ray650128/plugins/Routing.kt @@ -1,38 +1,92 @@ package com.ray650128.plugins -import com.ray650128.dto.PersonDto +import com.ray650128.JwtConfig +import com.ray650128.dto.UserDto import com.ray650128.extension.toDto -import com.ray650128.extension.toPerson +import com.ray650128.extension.toUser import com.ray650128.model.ErrorResponse -import com.ray650128.model.Person -import com.ray650128.service.PersonService +import com.ray650128.model.LoginResult +import com.ray650128.model.User +import com.ray650128.service.UserService import io.ktor.http.* import io.ktor.server.routing.* import io.ktor.server.response.* import io.ktor.server.application.* +import io.ktor.server.auth.* import io.ktor.server.request.* fun Application.configureRouting() { - val service = PersonService() + val service = UserService() routing { get("/") { call.respondText("Hello World!") } - route("/person") { - post { - val request = call.receive() - val person = request.toPerson() - service.create(person) - ?.let { userId -> - call.response.headers.append("My-User-Id-Header", userId.toString()) - call.respond(HttpStatusCode.Created) - } ?: call.respond(HttpStatusCode.BadRequest, ErrorResponse.BAD_REQUEST_RESPONSE) + route("/api") { + route("/v1") { + post("/register") { + val request = call.receive() + val newToken = JwtConfig.generateToken(request) + val user = request.toUser().apply { + token = newToken + createAt = System.currentTimeMillis() + } + service.create(user) + ?.let { userId -> + call.response.headers.append("My-User-Id-Header", userId.toString()) + call.respond(HttpStatusCode.Created, LoginResult(request.account, newToken)) + } ?: call.respond(HttpStatusCode.BadRequest, ErrorResponse.BAD_REQUEST_RESPONSE) + } + + post("/login") { + val request = call.receive() + val user = service.findByLoginInfo(request.account, request.password) + if (user != null) { + var token = user.token + if (token.isNullOrEmpty()) { + token = JwtConfig.generateToken(request) + user.token = token + user.updatedAt = System.currentTimeMillis() + service.updateById(user.id.toString(), user) + } + call.respond(HttpStatusCode.OK, LoginResult(request.account, token!!)) + } else { + call.respond( + status = HttpStatusCode.Unauthorized, + message = mapOf("message" to "Account or Password wrong.") + ) + } + } + + authenticate { + post("/logout") { + val account = call.authentication.principal()?.account ?: run { + call.respond( + status = HttpStatusCode.Unauthorized, + message = mapOf("message" to "token wrong") + ) + return@post + } + val user = service.findByAccount(account) ?: run { + call.respond( + status = HttpStatusCode.Unauthorized, + message = mapOf("message" to "token wrong") + ) + return@post + } + user.apply { + token = null + updatedAt = System.currentTimeMillis() + } + service.updateById(user.id.toString(), user) + call.respond(HttpStatusCode.OK, "User has logged out") + } + } } get { - val peopleList = service.findAll().map(Person::toDto) + val peopleList = service.findAll().map(User::toDto) call.respond(peopleList) } @@ -45,15 +99,15 @@ fun Application.configureRouting() { get("/search") { val name = call.request.queryParameters["name"].toString() - val foundPeople = service.findByName(name).map(Person::toDto) + val foundPeople = service.findByName(name).map(User::toDto) call.respond(foundPeople) } put("/{id}") { val id = call.parameters["id"].toString() - val personRequest = call.receive() - val person = personRequest.toPerson() - val updatedSuccessfully = service.updateById(id, person) + val userRequest = call.receive() + val user = userRequest.toUser() + val updatedSuccessfully = service.updateById(id, user) if (updatedSuccessfully) { call.respond(HttpStatusCode.NoContent) } else { diff --git a/src/main/kotlin/com/ray650128/service/PersonService.kt b/src/main/kotlin/com/ray650128/service/PersonService.kt deleted file mode 100644 index 65e8aed..0000000 --- a/src/main/kotlin/com/ray650128/service/PersonService.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.ray650128.service - -import com.ray650128.model.Person -import org.bson.types.ObjectId -import org.litote.kmongo.* -import org.litote.kmongo.id.toId - -class PersonService { - private val client = KMongo.createClient("mongodb://www.ray650128.com:27017") - private val database = client.getDatabase("person") - private val personCollection = database.getCollection() - - fun create(person: Person): Id? { - personCollection.insertOne(person) - return person.id - } - - fun findAll(): List = personCollection.find().toList() - - fun findById(id: String): Person? { - val bsonId: Id = ObjectId(id).toId() - return personCollection.findOne(Person::id eq bsonId) - } - - fun findByName(name: String): List { - val caseSensitiveTypeSafeFilter = Person::name regex name - return personCollection.find(caseSensitiveTypeSafeFilter).toList() - } - - fun updateById(id: String, request: Person): Boolean = - findById(id)?.let { person -> - val updateResult = personCollection.replaceOne(person.copy(name = request.name, age = request.age)) - updateResult.modifiedCount == 1L - } ?: false - - fun deleteById(id: String): Boolean { - val deleteResult = personCollection.deleteOneById(ObjectId(id)) - return deleteResult.deletedCount == 1L - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/ray650128/service/UserService.kt b/src/main/kotlin/com/ray650128/service/UserService.kt new file mode 100644 index 0000000..5dfcc1d --- /dev/null +++ b/src/main/kotlin/com/ray650128/service/UserService.kt @@ -0,0 +1,57 @@ +package com.ray650128.service + +import com.ray650128.model.User +import org.bson.types.ObjectId +import org.litote.kmongo.* +import org.litote.kmongo.id.toId + +class UserService { + private val client = KMongo.createClient("mongodb://www.ray650128.com:27017") + private val database = client.getDatabase("users") + private val userCollection = database.getCollection() + + fun create(user: User): Id? { + userCollection.insertOne(user) + return user.id + } + + fun findAll(): List = userCollection.find().toList() + + fun findById(id: String): User? { + val bsonId: Id = ObjectId(id).toId() + return userCollection.findOne(User::id eq bsonId) + } + + fun findByName(name: String): List { + val caseSensitiveTypeSafeFilter = User::name regex name + return userCollection.find(caseSensitiveTypeSafeFilter).toList() + } + + fun findByAccount(account: String): User? { + return userCollection.findOne(User::account eq account) + } + + fun findByLoginInfo(account: String, password: String): User? { + return userCollection.findOne( + User::account eq account, + User::password eq password + ) + } + + fun updateById(id: String, request: User): Boolean = + findById(id)?.let { user -> + val updateResult = userCollection.replaceOne( + user.copy( + name = request.name, + token = request.token, + updatedAt = request.updatedAt + ) + ) + updateResult.modifiedCount == 1L + } ?: false + + fun deleteById(id: String): Boolean { + val deleteResult = userCollection.deleteOneById(ObjectId(id)) + return deleteResult.deletedCount == 1L + } +} \ No newline at end of file