實作註冊、登入API

This commit is contained in:
Raymond Yang 2023-06-19 14:30:06 +08:00
parent 4bea8421e6
commit df2b630175
9 changed files with 273 additions and 4 deletions

View File

@ -1,6 +1,7 @@
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
val kmongo_version: String by project
plugins {
kotlin("jvm") version "1.8.22"
@ -32,4 +33,7 @@ dependencies {
implementation("ch.qos.logback:logback-classic:$logback_version")
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("org.litote.kmongo:kmongo-id-serialization:$kmongo_version")
}

View File

@ -2,3 +2,4 @@ ktor_version=2.3.1
kotlin_version=1.8.22
logback_version=1.2.11
kotlin.code.style=official
kmongo_version=4.5.0

View File

@ -0,0 +1,36 @@
package com.ray650128
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.ray650128.model.User
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 validityInMs = 300000 // 5 min
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: User): 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)
}

View File

@ -0,0 +1,42 @@
package com.ray650128.extensions
import com.ray650128.model.ErrorResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
suspend fun <T> ApplicationCall.sendCreated(data: T?) {
this.respond(
status = HttpStatusCode.Created,
message = data ?: mapOf("message" to "success.")
)
}
suspend fun <T : Any> ApplicationCall.sendSuccess(data: T?) {
this.respond(
status = HttpStatusCode.OK,
message = data ?: mapOf("message" to "success.")
)
}
suspend fun ApplicationCall.sendUnauthorized() {
this.respond(
status = HttpStatusCode.Unauthorized,
message = ErrorResponse.UNAUTHORIZED_RESPONSE
)
}
suspend fun ApplicationCall.sendBadRequest(errorResponse: ErrorResponse) {
this.respond(
status = HttpStatusCode.BadRequest,
message = errorResponse
)
}
suspend fun ApplicationCall.sendNotFound() {
this.respond(
status = HttpStatusCode.NotFound,
message = ErrorResponse.NOT_FOUND_RESPONSE
)
}

View File

@ -0,0 +1,12 @@
package com.ray650128.model
import kotlinx.serialization.Serializable
@Serializable
data class ErrorResponse(val message: String) {
companion object {
val NOT_FOUND_RESPONSE = ErrorResponse("Person was not found")
val BAD_REQUEST_RESPONSE = ErrorResponse("Invalid request")
val UNAUTHORIZED_RESPONSE = ErrorResponse("Unauthorized")
}
}

View File

@ -0,0 +1,9 @@
package com.ray650128.model
import kotlinx.serialization.Serializable
@Serializable
data class LoginResult(
var account: String,
var token: String
)

View File

@ -0,0 +1,17 @@
package com.ray650128.model
import io.ktor.server.auth.*
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.litote.kmongo.Id
import org.litote.kmongo.newId
@Serializable
data class User(
@Contextual val _id: Id<User>? = newId(),
val account: String,
val password: String,
var name: String? = null,
var createAt: Long? = null,
var updatedAt: Long? = null
): Principal

View File

@ -1,13 +1,105 @@
package com.ray650128.plugins
import io.ktor.server.routing.*
import io.ktor.server.response.*
import com.ray650128.JwtConfig
import com.ray650128.extensions.sendBadRequest
import com.ray650128.extensions.sendCreated
import com.ray650128.extensions.sendSuccess
import com.ray650128.extensions.sendUnauthorized
import com.ray650128.model.ErrorResponse
import com.ray650128.model.LoginResult
import com.ray650128.model.User
import com.ray650128.service.UserService
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
route("/api") {
route("/v1") {
post("/register") {
val request = call.receive<User>()
if (UserService.findByAccount(request.account) != null) {
call.sendBadRequest(ErrorResponse("User has existed."))
return@post
}
val newToken = JwtConfig.generateToken(request)
val user = request.apply {
createAt = System.currentTimeMillis()
}
UserService.create(user)?.let { userId ->
call.response.headers.append("My-User-Id-Header", userId.toString())
call.sendCreated(LoginResult(request.account, newToken))
} ?: call.sendBadRequest(ErrorResponse.BAD_REQUEST_RESPONSE)
}
post("/login") {
val request = call.receive<User>()
val user = UserService.findByLoginInfo(request.account, request.password)
if (user != null) {
val token = JwtConfig.generateToken(request)
call.sendSuccess(LoginResult(request.account, token))
} else {
call.sendBadRequest(ErrorResponse("Account or Password wrong."))
}
}
authenticate {
post("/logout") {
val account = call.authentication.principal<User>()?.account ?: run {
call.sendUnauthorized()
return@post
}
val user = UserService.findByAccount(account) ?: run {
call.sendUnauthorized()
return@post
}
UserService.updateById(user._id.toString(), user)
call.sendSuccess(null)
}
}
}
/*get {
val peopleList = UserService.findAll()
call.respond(peopleList)
}
get("/{id}") {
val id = call.parameters["id"].toString()
UserService.findById(id)
?.let { foundPerson -> call.respond(foundPerson) }
?: call.sendNotFound()
}
get("/search") {
val name = call.request.queryParameters["name"].toString()
val foundPeople = UserService.findByName(name)
call.respond(foundPeople)
}
put("/{id}") {
val id = call.parameters["id"].toString()
val userRequest = call.receive<User>()
val updatedSuccessfully = UserService.updateById(id, userRequest)
if (updatedSuccessfully) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.BadRequest, ErrorResponse.BAD_REQUEST_RESPONSE)
}
}
delete("/{id}") {
val id = call.parameters["id"].toString()
val deletedSuccessfully = UserService.deleteById(id)
if (deletedSuccessfully) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ErrorResponse.NOT_FOUND_RESPONSE)
}
}*/
}
}
}

View File

@ -0,0 +1,56 @@
package com.ray650128.service
import com.ray650128.model.User
import org.bson.types.ObjectId
import org.litote.kmongo.*
import org.litote.kmongo.id.toId
object UserService {
private val client = KMongo.createClient("mongodb://ray650128:Zx650128!@www.ray650128.com:27017")
private val database = client.getDatabase("test_system")
private val userCollection = database.getCollection<User>()
fun create(user: User): Id<User>? {
userCollection.insertOne(user)
return user._id
}
fun findAll(): List<User> = userCollection.find().toList()
fun findById(id: String): User? {
val bsonId: Id<User> = ObjectId(id).toId()
return userCollection.findOne(User::_id eq bsonId)
}
fun findByName(name: String): List<User> {
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,
updatedAt = request.updatedAt
)
)
updateResult.modifiedCount == 1L
} ?: false
fun deleteById(id: String): Boolean {
val deleteResult = userCollection.deleteOneById(ObjectId(id))
return deleteResult.deletedCount == 1L
}
}