初步加上帳號服務

This commit is contained in:
Raymond Yang 2023-05-15 17:03:13 +08:00
parent 7c2839cade
commit 169278fc1d
13 changed files with 250 additions and 97 deletions

View File

@ -31,4 +31,6 @@ dependencies {
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
implementation("org.litote.kmongo:kmongo:$kmongo_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")
} }

View File

@ -1,12 +1,31 @@
package com.ray650128 package com.ray650128
import com.ray650128.dto.UserDto
import com.ray650128.model.User
import com.ray650128.plugins.configureRouting import com.ray650128.plugins.configureRouting
import com.ray650128.plugins.configureSerialization 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.engine.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
fun main() { fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") { 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() configureRouting()
configureSerialization() configureSerialization()
}.start(wait = true) }.start(wait = true)

View File

@ -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)
}

View File

@ -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
)

View File

@ -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

View File

@ -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
)

View File

@ -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
)

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

@ -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<Person>? = null,
val name: String,
val age: Int
)

View File

@ -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<User>? = null,
val account: String,
val password: String,
var name: String? = null,
var token: String? = null,
var createAt: Long? = null,
var updatedAt: Long? = null
)

View File

@ -1,38 +1,92 @@
package com.ray650128.plugins 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.toDto
import com.ray650128.extension.toPerson import com.ray650128.extension.toUser
import com.ray650128.model.ErrorResponse import com.ray650128.model.ErrorResponse
import com.ray650128.model.Person import com.ray650128.model.LoginResult
import com.ray650128.service.PersonService import com.ray650128.model.User
import com.ray650128.service.UserService
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.* import io.ktor.server.request.*
fun Application.configureRouting() { fun Application.configureRouting() {
val service = PersonService() val service = UserService()
routing { routing {
get("/") { get("/") {
call.respondText("Hello World!") call.respondText("Hello World!")
} }
route("/person") { route("/api") {
post { route("/v1") {
val request = call.receive<PersonDto>() post("/register") {
val person = request.toPerson() val request = call.receive<UserDto>()
service.create(person) val newToken = JwtConfig.generateToken(request)
val user = request.toUser().apply {
token = newToken
createAt = System.currentTimeMillis()
}
service.create(user)
?.let { userId -> ?.let { userId ->
call.response.headers.append("My-User-Id-Header", userId.toString()) call.response.headers.append("My-User-Id-Header", userId.toString())
call.respond(HttpStatusCode.Created) call.respond(HttpStatusCode.Created, LoginResult(request.account, newToken))
} ?: call.respond(HttpStatusCode.BadRequest, ErrorResponse.BAD_REQUEST_RESPONSE) } ?: call.respond(HttpStatusCode.BadRequest, ErrorResponse.BAD_REQUEST_RESPONSE)
} }
post("/login") {
val request = call.receive<UserDto>()
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<UserDto>()?.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 { get {
val peopleList = service.findAll().map(Person::toDto) val peopleList = service.findAll().map(User::toDto)
call.respond(peopleList) call.respond(peopleList)
} }
@ -45,15 +99,15 @@ fun Application.configureRouting() {
get("/search") { get("/search") {
val name = call.request.queryParameters["name"].toString() 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) call.respond(foundPeople)
} }
put("/{id}") { put("/{id}") {
val id = call.parameters["id"].toString() val id = call.parameters["id"].toString()
val personRequest = call.receive<PersonDto>() val userRequest = call.receive<UserDto>()
val person = personRequest.toPerson() val user = userRequest.toUser()
val updatedSuccessfully = service.updateById(id, person) val updatedSuccessfully = service.updateById(id, user)
if (updatedSuccessfully) { if (updatedSuccessfully) {
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} else { } else {

View File

@ -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<Person>()
fun create(person: Person): Id<Person>? {
personCollection.insertOne(person)
return person.id
}
fun findAll(): List<Person> = personCollection.find().toList()
fun findById(id: String): Person? {
val bsonId: Id<Person> = ObjectId(id).toId()
return personCollection.findOne(Person::id eq bsonId)
}
fun findByName(name: String): List<Person> {
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
}
}

View File

@ -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<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,
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
}
}