From a9c80b162148ea604eb581fff913bb8dd22420fc Mon Sep 17 00:00:00 2001 From: Julien Lengrand-Lambert Date: Wed, 17 Aug 2022 16:27:36 +0200 Subject: [PATCH] Adds functional login, signup and auth --- src/main/js/pluckr-app/src/PluckrLogin.js | 83 ++++++++++++++++++- .../kotlin/nl/lengrand/pluckr/Application.kt | 62 ++++---------- .../nl/lengrand/pluckr/TreeController.kt | 47 +++++++---- .../nl/lengrand/pluckr/plugins/Routing.kt | 37 +++++---- 4 files changed, 153 insertions(+), 76 deletions(-) diff --git a/src/main/js/pluckr-app/src/PluckrLogin.js b/src/main/js/pluckr-app/src/PluckrLogin.js index 805a09f..687c76c 100644 --- a/src/main/js/pluckr-app/src/PluckrLogin.js +++ b/src/main/js/pluckr-app/src/PluckrLogin.js @@ -18,6 +18,7 @@ export class PluckrLogin extends LitElement { static get properties() { return { signUpOpened: {type: Boolean}, + logInOpened: {type: Boolean}, username: {type: String}, password: {type: String}, passwordConfirm: {type: String}, @@ -33,6 +34,7 @@ export class PluckrLogin extends LitElement { constructor() { super(); this.signUpOpened = false; + this.logInOpened = false; this.loadingBarActive = false; } @@ -40,7 +42,7 @@ export class PluckrLogin extends LitElement { return html` Sign up - Login + Login @@ -50,14 +52,52 @@ export class PluckrLogin extends LitElement { @opened-changed="${e => (this.signUpOpened = e.detail.value)}" ${dialogRenderer(this.signUpRenderer, [this.username, this.signUpOpened, this.password, this.loadingBarActive, this.errorMessage])} id="signUp"> + + `; } signUpClicked() { this.signUpOpened = true; - console.log("Signup clicked!"); + } + + logInClicked() { + this.logInOpened = true; + } + + logInRenderer(){ + return html` + + + + + ${ this.loadingBarActive ? + html`` + : undefined + } + + ${ this.errorMessage ? + html`

${this.errorMessage}

` + : undefined + } + + + Cancel + LogIn + +
+ `; } signUpRenderer() { @@ -94,6 +134,42 @@ export class PluckrLogin extends LitElement { `; } + logIn(){ + this.loadingBarActive = true; + + fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + 'username': this.username, + 'password': this.password, + }) + }) + .then((response) => { + this.loadingBarActive = false; + if (!response.ok) { + this.errorMessage = response.statusText; + console.error('There has been a problem logging in the user:', response.statusText); + } + else{ + this.username = null; + this.password = null; + this.passwordConfirm = null; + this.logInOpened = false; + } + }) + .catch(error => { + this.errorMessage = "There has been an issue contacting the server. Please try again later" + console.error('There has been a problem with your fetch operation:', error); + this.username = null; + this.password = null; + this.passwordConfirm = null; + }); + } + + signUp() { this.loadingBarActive = true; @@ -123,6 +199,9 @@ export class PluckrLogin extends LitElement { .catch(error => { this.errorMessage = "There has been an issue contacting the server. Please try again later" console.error('There has been a problem with your fetch operation:', error); + this.username = null; + this.password = null; + this.passwordConfirm = null; }); } } diff --git a/src/main/kotlin/nl/lengrand/pluckr/Application.kt b/src/main/kotlin/nl/lengrand/pluckr/Application.kt index 5f54d0f..c6ba48c 100644 --- a/src/main/kotlin/nl/lengrand/pluckr/Application.kt +++ b/src/main/kotlin/nl/lengrand/pluckr/Application.kt @@ -5,43 +5,49 @@ import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.engine.* -import io.ktor.server.metrics.micrometer.* import io.ktor.server.netty.* import io.ktor.server.plugins.callloging.* import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.plugins.cors.routing.* import io.ktor.server.response.* import io.ktor.server.sessions.* import kotlinx.serialization.json.Json -import net.postgis.jdbc.geometry.Point -import nl.lengrand.pluckr.plugins.* -import org.jetbrains.exposed.sql.* +import nl.lengrand.pluckr.plugins.configureRouting +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.transactions.transaction -import org.mindrot.jbcrypt.BCrypt -import org.mindrot.jbcrypt.BCrypt.* fun Application.myapp(){ val database = initDb() install(Sessions) { - cookie("user_session") { + cookie("user_session", SessionStorageMemory()) { cookie.path = "/" cookie.maxAgeInSeconds = 6000 } } install(Authentication) { - session("auth-session") { + session("user_session") { + println("validating session!") + + validate { session -> - if(session.name.startsWith("jet")) { + if(session.name.isNotEmpty()) { + println("Valid!") + println(session) session } else { + println("Not valid!") + println(session) null } } challenge { - call.respondRedirect("/login") + println("redirecting") + call.respondRedirect("/") } } } @@ -55,7 +61,6 @@ fun Application.myapp(){ } install(CallLogging) // install(MicrometerMetrics) - configureRouting(database) } @@ -65,40 +70,7 @@ fun initDb(): Database { transaction { addLogger(StdOutSqlLogger) - SchemaUtils.create(Trees, Users) - -// val salt = gensalt() -// println(salt) -// val user = Users.insert { -// it[username] = "bobby2@gmail.com" -// it[password] = hashpw("aVerySecretPassword", salt); -// } - -// Users.selectAll().map { -// println("${it[Users.username]}, ${it[Users.password]}, ${it[Users.createdAt]}, ${it[Users.updatedAt]}") -//// if (checkpw("booby", it[Users.password])) -//// println("It matches"); -//// else -//// println("It does not match"); -// println("-----") -// } - -// val first = Tree.new { -// name = "Laurier" -// description = "un laurier accessible à tous" -// location = Point(52.04681865145196, 5.079779509938945) -// } - -// Trees.insert { -// it[name] = "Laurier 2" -// it[description] = "un laurier accessible à tous" -// it[location] = Point(52.04681865145196, 5.079779509938945) -// } - - -// println("Trees: ${Tree.all().joinToString {it.location.value}}") - } return database } diff --git a/src/main/kotlin/nl/lengrand/pluckr/TreeController.kt b/src/main/kotlin/nl/lengrand/pluckr/TreeController.kt index 291c7d3..d44e9a3 100644 --- a/src/main/kotlin/nl/lengrand/pluckr/TreeController.kt +++ b/src/main/kotlin/nl/lengrand/pluckr/TreeController.kt @@ -7,15 +7,12 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import net.postgis.jdbc.geometry.Point import nl.lengrand.pluckr.Trees -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toKotlinLocalDateTime import nl.lengrand.pluckr.Users -import org.jetbrains.exposed.sql.insert -import org.mindrot.jbcrypt.BCrypt.gensalt -import org.mindrot.jbcrypt.BCrypt.hashpw +import org.jetbrains.exposed.sql.* +import org.mindrot.jbcrypt.BCrypt.* @Serializable data class UserSession(val name: String) : Principal @@ -56,8 +53,12 @@ object PointSerializer : KSerializer { } } -private fun fromRow(it: ResultRow): Tree { - return Tree(it[Trees.id], it[Trees.name], it[Trees.description], it[Trees.location]) +private fun ResultRow.toTree(): Tree { + return Tree(this[Trees.id], this[Trees.name], this[Trees.description], this[Trees.location]) +} + +private fun ResultRow.toUser(): User { + return User(this[Users.id], this[Users.username], this[Users.password], this[Users.createdAt].toKotlinLocalDateTime(), this[Users.updatedAt].toKotlinLocalDateTime()) } class UserController(private val database: Database){ @@ -65,25 +66,41 @@ class UserController(private val database: Database){ val salt = gensalt() transaction(database) { Users.insert { - it[username] = email - it[password] = hashpw(zepassword, salt); - } + it[username] = email + it[password] = hashpw(zepassword, salt); } - } } + /* + Will throw NoSuchElementException if there are no results, or IllegalArgumentException if there are more than one + */ + private fun getUser(email: String): User { + val user = transaction(database) { + Users.select{ Users.username eq email}.single().toUser() + } + return user + } + + fun getUser(email: String, zepassword: String): User { + val user = getUser(email) + if (!checkpw(zepassword, user.password)) throw AuthenticationException("Incorrect password detected") + return user + } +} + class TreeController(private val database: Database) { fun getTrees() : ArrayList { val trees : ArrayList = arrayListOf() transaction(database){ - Trees.selectAll().map { trees.add(fromRow(it)) } + Trees.selectAll().map { trees.add(it.toTree()) } } return trees } fun getTrees(bbox: List?) : ArrayList { - println(bbox) return getTrees() } -} \ No newline at end of file +} + +class AuthenticationException(message:String): Exception(message) diff --git a/src/main/kotlin/nl/lengrand/pluckr/plugins/Routing.kt b/src/main/kotlin/nl/lengrand/pluckr/plugins/Routing.kt index fb8e156..c87cc9d 100644 --- a/src/main/kotlin/nl/lengrand/pluckr/plugins/Routing.kt +++ b/src/main/kotlin/nl/lengrand/pluckr/plugins/Routing.kt @@ -13,7 +13,6 @@ import io.ktor.server.response.* import io.ktor.server.sessions.* import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.sql.Database -import java.sql.SQLIntegrityConstraintViolationException fun Application.configureRouting(database: Database) { @@ -22,21 +21,37 @@ fun Application.configureRouting(database: Database) { routing { - authenticate("auth-form") { - post("/api/login") { - val userName = call.principal()?.name.toString() - call.sessions.set(UserSession(name = userName)) - call.respondRedirect("/hello") + authenticate("user_session") { + get("/api/authenticated"){ + call.respondText("Hello, ${call.principal()?.name}!") } } + post("/api/login") { + val formParameters = call.receiveParameters() + + try{ + val user = userController.getUser(formParameters["username"].toString(), formParameters["password"].toString()) + call.sessions.set(UserSession(user.username)) + call.respondRedirect("/") + } + catch(e: ExposedSQLException){ + call.response.status(HttpStatusCode(500, e.message!!)) + } + } + + post("/api/logout") { + call.sessions.clear() + call.respondRedirect("/") + } + post("/api/signup"){ val formParameters = call.receiveParameters() try{ userController.createUser(formParameters["username"].toString(), formParameters["password"].toString()) call.response.status(HttpStatusCode.OK) } - catch(e: ExposedSQLException){ + catch(e: ExposedSQLException){ // TODO: Should I leak exceptions here? val message = when (e.sqlState) { "23505" -> "User already exists" @@ -44,15 +59,11 @@ fun Application.configureRouting(database: Database) { "Unknown error, please retry later" } call.response.status(HttpStatusCode(500, message)) - } - } get("/api/trees") { - println("IN HERE FIRST") if(call.request.queryParameters["bbox"] != null){ - println("IN HERE") val bbox = call.request.queryParameters["bbox"]?.split(",")?.map { it.toDouble() } call.respond(treeController.getTrees(bbox)) } @@ -61,9 +72,7 @@ fun Application.configureRouting(database: Database) { } } - - - get("/hello") { + get("/api/hello") { call.respondText("Hello the World!") }