Merge pull request 'FEAT-addition-security' (#4) from FEAT-addition-security into main
Reviewed-on: https://gitea.com/NickKalar/LMS-APIs/pulls/4
This commit is contained in:
@@ -16,7 +16,7 @@ import io.ktor.server.response.respond
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
embeddedServer(Netty, port = 8080) {
|
||||
embeddedServer(Netty, host = "127.0.0.1", port = 8080) {
|
||||
install(CORS) {
|
||||
anyHost()
|
||||
allowHeader(HttpHeaders.ContentType)
|
||||
@@ -39,7 +39,7 @@ fun Application.module() {
|
||||
}
|
||||
|
||||
install(Authentication) {
|
||||
jwt("auth-jwt") {
|
||||
jwt("general") {
|
||||
realm = myRealm
|
||||
verifier(
|
||||
JWT
|
||||
@@ -55,7 +55,45 @@ fun Application.module() {
|
||||
}
|
||||
}
|
||||
challenge { defaultScheme, realm ->
|
||||
call.respond(HttpStatusCode.Unauthorized, "${defaultScheme}, $realm Token is not valid or has expired")
|
||||
call.respond(HttpStatusCode.Unauthorized, "$defaultScheme, $realm Token is not valid or has expired")
|
||||
}
|
||||
}
|
||||
jwt("patron"){
|
||||
realm = myRealm
|
||||
verifier(
|
||||
JWT
|
||||
.require(Algorithm.HMAC256(secret))
|
||||
.withAudience(audience)
|
||||
.withIssuer(issuer)
|
||||
.build())
|
||||
validate { credential ->
|
||||
if (credential.payload.getClaim("role").asString() != "patron") {
|
||||
JWTPrincipal(credential.payload)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge { _, _ ->
|
||||
call.respond(HttpStatusCode.Unauthorized, "Insufficient permissions to access this resource.")
|
||||
}
|
||||
}
|
||||
jwt("staff"){
|
||||
realm = myRealm
|
||||
verifier(
|
||||
JWT
|
||||
.require(Algorithm.HMAC256(secret))
|
||||
.withAudience(audience)
|
||||
.withIssuer(issuer)
|
||||
.build())
|
||||
validate { credential ->
|
||||
if (credential.payload.getClaim("role").asString() != "staff") {
|
||||
JWTPrincipal(credential.payload)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge { _, _ ->
|
||||
call.respond(HttpStatusCode.Unauthorized, "Insufficient permissions to access this resource.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ fun Application.configureCollectionItemRoutes(dbConnection: Connection) {
|
||||
}
|
||||
}
|
||||
|
||||
authenticate("auth-jwt") {
|
||||
authenticate("staff") {
|
||||
post("/items") {
|
||||
try {
|
||||
val item = call.receive<NewCollectionItem>()
|
||||
|
||||
@@ -7,6 +7,7 @@ import codes.kalar.model.NewLibrary
|
||||
import codes.kalar.service.LibraryService
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
@@ -41,7 +42,10 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) {
|
||||
} catch (cause: DbElementNotFoundException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to find Library.")
|
||||
} catch (cause: NumberFormatException) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Unable to parse number format. \"${call.pathParameters["id"]}\" is not a number.")
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
"Unable to parse number format. \"${call.pathParameters["id"]}\" is not a number."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,43 +53,44 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) {
|
||||
// TODO Add search for collection_it where itemID && libraryID
|
||||
}
|
||||
|
||||
post("/libraries") {
|
||||
val library = call.receive<NewLibrary>()
|
||||
try {
|
||||
val id = libraryService.create(library)
|
||||
call.respondText("${library.name} is posted with the ID: $id")
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to insert Library.")
|
||||
}
|
||||
}
|
||||
|
||||
patch("/libraries") {
|
||||
try {
|
||||
val library = call.receive<Library>()
|
||||
val patchedLibrary = libraryService.update(library)
|
||||
call.respond(HttpStatusCode.OK, patchedLibrary)
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
log.error(cause.message)
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Library.")
|
||||
} catch (cause: ContentTransformationException) {
|
||||
log.error(cause.message)
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||
authenticate("staff") {
|
||||
post("/libraries") {
|
||||
val library = call.receive<NewLibrary>()
|
||||
try {
|
||||
val id = libraryService.create(library)
|
||||
call.respondText("${library.name} is posted with the ID: $id")
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to insert Library.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
patch("/libraries") {
|
||||
try {
|
||||
val library = call.receive<Library>()
|
||||
val patchedLibrary = libraryService.update(library)
|
||||
call.respond(HttpStatusCode.OK, patchedLibrary)
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
log.error(cause.message)
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Library.")
|
||||
} catch (cause: ContentTransformationException) {
|
||||
log.error(cause.message)
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||
}
|
||||
}
|
||||
|
||||
delete("/libraries") {
|
||||
try {
|
||||
val id = call.parameters["id"]!!.toLong()
|
||||
log.info("Deleting item with id=$id")
|
||||
libraryService.delete(id)
|
||||
call.respondText(":(", status = HttpStatusCode.OK)
|
||||
} catch (cause: DbElementNotFoundException) {
|
||||
log.error(cause.message, cause)
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||
} catch (cause: NumberFormatException) {
|
||||
log.error(cause.message, cause)
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Invalid ID format")
|
||||
delete("/libraries") {
|
||||
try {
|
||||
val id = call.parameters["id"]!!.toLong()
|
||||
log.info("Deleting item with id=$id")
|
||||
libraryService.delete(id)
|
||||
call.respondText(":(", status = HttpStatusCode.OK)
|
||||
} catch (cause: DbElementNotFoundException) {
|
||||
log.error(cause.message, cause)
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||
} catch (cause: NumberFormatException) {
|
||||
log.error(cause.message, cause)
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Invalid ID format")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package codes.kalar.routes
|
||||
import codes.kalar.exception.DbElementNotFoundException
|
||||
import codes.kalar.model.User
|
||||
import codes.kalar.service.PatronService
|
||||
import codes.kalar.service.StaffService
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import io.ktor.http.HttpStatusCode
|
||||
@@ -18,8 +19,10 @@ fun Application.configureLoginRoutes(dbConnection: Connection) {
|
||||
val secret = environment.config.property("jwt.secret").getString()
|
||||
val issuer = environment.config.property("jwt.issuer").getString()
|
||||
val audience = environment.config.property("jwt.audience").getString()
|
||||
val auth0Map = mapOf("secret" to secret, "issuer" to issuer, "audience" to audience)
|
||||
|
||||
val patronService = PatronService(dbConnection)
|
||||
val staffService = StaffService(dbConnection)
|
||||
routing {
|
||||
post("/login") {
|
||||
try {
|
||||
@@ -28,20 +31,35 @@ fun Application.configureLoginRoutes(dbConnection: Connection) {
|
||||
val password = user.password
|
||||
|
||||
if (patronService.loginPatronByLoginUsername(name, password)) {
|
||||
val token = JWT.create()
|
||||
.withAudience(audience)
|
||||
.withIssuer(issuer)
|
||||
.withClaim("name", name)
|
||||
.withExpiresAt(Date(System.currentTimeMillis() + 160000))
|
||||
.sign(Algorithm.HMAC256(secret))
|
||||
call.respond(hashMapOf("token" to token))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.Unauthorized, "Invalid login")
|
||||
val token = createToken(user.name, "patron", auth0Map)
|
||||
call.respond(HttpStatusCode.OK, mapOf("token" to token))
|
||||
}
|
||||
else if (staffService.loginStaffByLoginUsername(name, password)) {
|
||||
val token = createToken(user.name, "staff", auth0Map)
|
||||
call.respond(HttpStatusCode.OK, mapOf("token" to token))
|
||||
}
|
||||
else {
|
||||
log.error("Unauthorized use: $name")
|
||||
call.respond(HttpStatusCode.Unauthorized, mapOf("message" to "Invalid login", "User" to user.name))
|
||||
}
|
||||
} catch (cause: DbElementNotFoundException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Something went wrong")
|
||||
log.error(cause.message)
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("message" to cause.message))
|
||||
} catch (cause: Exception) {
|
||||
log.error(cause.message)
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("message" to "An unexpected error occurred: ${cause.message}"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createToken(username: String, role: String, auth: Map<String, String>): String {
|
||||
return JWT.create()
|
||||
.withAudience(auth["audience"])
|
||||
.withIssuer(auth["issuer"])
|
||||
.withClaim("name", username)
|
||||
.withClaim("role", role)
|
||||
.withExpiresAt(Date(System.currentTimeMillis() + 160000))
|
||||
.sign(Algorithm.HMAC256(auth["secret"]))
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import codes.kalar.model.Patron
|
||||
import codes.kalar.service.PatronService
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
@@ -42,39 +43,49 @@ fun Application.configurePatronRoutes(dbConnection: Connection) {
|
||||
}
|
||||
}
|
||||
|
||||
post("/patron") {
|
||||
try {
|
||||
val patron = call.receive<NewPatron>()
|
||||
val id = patronService.create(patron)
|
||||
call.respondText("Adding ${patron.name} to database with the id of $id", status = HttpStatusCode.OK)
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||
} catch (cause: ContentTransformationException) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid CollectionItem object.")
|
||||
authenticate("staff") {
|
||||
post("/patron") {
|
||||
try {
|
||||
val patron = call.receive<NewPatron>()
|
||||
val id = patronService.create(patron)
|
||||
call.respondText("Adding ${patron.name} to database with the id of $id", status = HttpStatusCode.OK)
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||
} catch (cause: ContentTransformationException) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid CollectionItem object.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patch("/patron") {
|
||||
try {
|
||||
val patron = call.receive<Patron>()
|
||||
val patchedPatron = patronService.update(patron)
|
||||
call.respondText("${patron.name} is patched")
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Patron.")
|
||||
} catch (cause: ContentTransformationException) {
|
||||
|
||||
authenticate("general") {
|
||||
patch("/patron") {
|
||||
try {
|
||||
val patron = call.receive<Patron>()
|
||||
val isPatched = patronService.update(patron)
|
||||
if (isPatched) {
|
||||
call.respond(HttpStatusCode.OK, "${patron.name} is patched")
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, "${patron.name} is not patched")
|
||||
}
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Patron.")
|
||||
} catch (cause: ContentTransformationException) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid Patron object.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete("/patron/{id}") {
|
||||
try {
|
||||
val id = call.pathParameters["id"]!!.toLong()
|
||||
patronService.delete(id)
|
||||
call.respond(HttpStatusCode.OK, "Successfully deleted the patron")
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to delete Patron.")
|
||||
} catch (cause: NumberFormatException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "ID needs to be a number.")
|
||||
authenticate("staff") {
|
||||
delete("/patron/{id}") {
|
||||
try {
|
||||
val id = call.pathParameters["id"]!!.toLong()
|
||||
patronService.delete(id)
|
||||
call.respond(HttpStatusCode.OK, "Successfully deleted the patron")
|
||||
} catch (cause: DbElementInsertionException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to delete Patron.")
|
||||
} catch (cause: NumberFormatException) {
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "ID needs to be a number.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package codes.kalar.routes
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import java.sql.Connection
|
||||
@@ -16,16 +17,18 @@ fun Application.configureStaffRoutes(dbConnection: Connection) {
|
||||
call.respondText(call.parameters["id"]!!)
|
||||
}
|
||||
|
||||
post("/staff") {
|
||||
authenticate("staff") {
|
||||
post("/staff") {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
patch("/staff") {
|
||||
patch("/staff") {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
delete("/staff/{id}") {
|
||||
delete("/staff/{id}") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,17 @@ class StaffService(private val connection: Connection) {
|
||||
private const val SELECT_STAFF_BY_ID = ""
|
||||
private const val INSERT_STAFF = ""
|
||||
private const val UPDATE_STAFF_BY_ID = ""
|
||||
// In the event are "deleted" erroneously, having a flag set instead of actually removing the entry allows
|
||||
// In the event staff are "deleted" erroneously, having a flag set instead of actually removing the entry allows
|
||||
// for quick reversal.
|
||||
private const val ARCHIVE_STAFF_BY_ID = ""
|
||||
}
|
||||
|
||||
suspend fun create() {}
|
||||
|
||||
fun loginStaffByLoginUsername(username: String, password: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun read() {}
|
||||
|
||||
suspend fun update() {}
|
||||
|
||||
Reference in New Issue
Block a user