From 0eb3ce33d0eb11cdc849eb980c1a55b79783874d Mon Sep 17 00:00:00 2001 From: aldemirm Date: Mon, 14 Sep 2026 12:10:33 +0200 Subject: [PATCH] - password validation - Adding and refactoring tests --- .../oliver/kidio/backend/plugins/Routing.kt | 328 +++++++++--------- src/test/kotlin/ServerTest.kt | 59 ++-- 2 files changed, 193 insertions(+), 194 deletions(-) diff --git a/src/main/kotlin/com/oliver/kidio/backend/plugins/Routing.kt b/src/main/kotlin/com/oliver/kidio/backend/plugins/Routing.kt index 3623301..9f9d595 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/plugins/Routing.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/plugins/Routing.kt @@ -17,207 +17,203 @@ fun isValidPassword(password: String): Boolean { if (password.length < 8) return false val hasUppercase = password.any { it.isUpperCase() } - val hasEmoji = password.any { char -> - val type = Character.getType(char).toByte() - type == Character.SURROGATE || + val hasEmoji = + password.any { char -> + val type = Character.getType(char).toByte() + type == Character.SURROGATE || type == Character.OTHER_SYMBOL || char.code in 0x1F600..0x1F64F || char.code in 0x1F300..0x1F5FF || char.code in 0x1F680..0x1F6FF || char.code in 0x2600..0x26FF - } + } val hasSpecialChar = password.any { !it.isLetterOrDigit() && !hasEmoji } return hasUppercase && hasSpecialChar && !hasEmoji } -fun Application.configureRouting(repository: KidioRepository, jwtService : JwtService) { - fun Application.configureRouting( - repository: KidioRepository, - jwtService: JwtService, - ) { - routing { - post("/api/v1/auth/register") { - val registerRequest = call.receive() - if (registerRequest.email.isBlank() || registerRequest.passwordPlain.isBlank()) { - call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus") - return@post - } - if (!isValidPassword(registerRequest.passwordPlain)) { - call.respond( - HttpStatusCode.BadRequest, - "Das Passwort muss mindestens 8 Zeichen lang sein, mindestens einen Großbuchstaben und ein Sonderzeichen enthalten und darf keine Emojis enthalten." - ) - return@post - } - if (registerRequest.parentPin.length != 4) { - call.respond(HttpStatusCode.BadRequest, "Die Eltern-PIN muss genau 4 Ziffern lang sein") - return@post - } - val existingUser = repository.findByEmail(registerRequest.email) - if (existingUser != null) { - call.respond(HttpStatusCode.Conflict, "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits") - return@post - } - - // Backend generiert die eindeutige User-ID - val userId = UUID.randomUUID().toString() - val passwordHash = BCrypt.hashpw(registerRequest.passwordPlain, BCrypt.gensalt()) - - // datenbank insert hinzufügen - repository.registerUser( - User( - userId, - registerRequest.email, - passwordHash, - registerRequest.familyName, - registerRequest.childAge, - registerRequest.parentPin, - ), - ) - - val token = jwtService.generateToken(userId) - - call.respond( - HttpStatusCode.Created, - mapOf( - "token" to token, - "userId" to userId, - "message" to "Registrierung erfolgreich", - ), - ) +fun Application.configureRouting( + repository: KidioRepository, + jwtService: JwtService, +) { + routing { + post("/api/v1/auth/register") { + val registerRequest = call.receive() + if (registerRequest.email.isBlank() || registerRequest.passwordPlain.isBlank()) { + call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus") + return@post } - // Health-Check - get("/api/v1/health") { - call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!")) - } - - post("/api/v1/auth/login") { - val loginRequest = call.receive() - if (loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()) { - call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus") - return@post - } - val getUser = repository.findByEmail(loginRequest.email) - if (getUser == null || !repository.verifyPassword(loginRequest.passwordPlain, getUser.passwordHash)) { - call.respond(HttpStatusCode.Unauthorized, "Ungültige Anmeldedaten") - return@post - } - val token = jwtService.generateToken(getUser.id) + if (!isValidPassword(registerRequest.passwordPlain)) { call.respond( - HttpStatusCode.OK, - AuthResponse( - token, - userId = getUser.id, - familyName = getUser.familyName, - childAge = getUser.childAge, - ), + HttpStatusCode.BadRequest, + "Das Passwort muss mindestens 8 Zeichen lang sein, " + + "mindestens einen Großbuchstaben und ein Sonderzeichen enthalten.", ) return@post - - + } + if (registerRequest.parentPin.length != 4) { + call.respond(HttpStatusCode.BadRequest, "Die Eltern-PIN muss genau 4 Ziffern lang sein") + return@post + } + val existingUser = repository.findByEmail(registerRequest.email) + if (existingUser != null) { + call.respond(HttpStatusCode.Conflict, "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits") + return@post } - authenticate("auth-jwt") { + // Backend generiert die eindeutige User-ID + val userId = UUID.randomUUID().toString() + val passwordHash = BCrypt.hashpw(registerRequest.passwordPlain, BCrypt.gensalt()) - // 1. Alle verfügbaren Aufgaben abrufen - get("/api/v1/tasks") { - val tasks = repository.getAllTasks() - call.respond(tasks) - } + // datenbank insert hinzufügen + repository.registerUser( + User( + userId, + registerRequest.email, + passwordHash, + registerRequest.familyName, + registerRequest.childAge, + registerRequest.parentPin, + ), + ) - // 2. Antwort auf eine Aufgabe überprüfen - post("/api/v1/tasks/verify") { - val request = call.receive() - val principal = call.principal() - val userId = principal?.userId - if (userId == null) { - call.respond( - HttpStatusCode.Unauthorized, - mapOf("error" to "Ungültiges oder abgelaufenes Token") - ) - return@post - } - val taskInfo = repository.getTaskAnswerAndReward(request.taskId) - if (taskInfo == null) { - call.respond( - TaskVerifyResponse( - isCorrect = false, - earnedMinutes = 0, - message = "Aufgabe nicht gefunden!", - ), - ) - return@post - } + val token = jwtService.generateToken(userId) - val (correctAnswer, reward) = taskInfo - val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim() + call.respond( + HttpStatusCode.Created, + mapOf( + "token" to token, + "userId" to userId, + "message" to "Registrierung erfolgreich", + ), + ) + } + // Health-Check + get("/api/v1/health") { + call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!")) + } - if (isCorrect) { - val currentScreentime = repository.getScreentime(userId) - val newSeconds = currentScreentime + (reward * 60) - repository.updateScreentime(userId, newSeconds) + post("/api/v1/auth/login") { + val loginRequest = call.receive() + if (loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()) { + call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus") + return@post + } + val getUser = repository.findByEmail(loginRequest.email) + if (getUser == null || !repository.verifyPassword(loginRequest.passwordPlain, getUser.passwordHash)) { + call.respond(HttpStatusCode.Unauthorized, "Ungültige Anmeldedaten") + return@post + } + val token = jwtService.generateToken(getUser.id) + call.respond( + HttpStatusCode.OK, + AuthResponse( + token, + userId = getUser.id, + familyName = getUser.familyName, + childAge = getUser.childAge, + ), + ) + return@post + } - call.respond( - TaskVerifyResponse( - isCorrect = true, - earnedMinutes = reward, - message = "Super gemacht! Du hast $reward Minuten gewonnen.", - ), - ) - } else { - call.respond( - TaskVerifyResponse( - isCorrect = false, - earnedMinutes = 0, - message = "Schade, das war leider falsch. Versuche es nochmal!", - ), - ) - } - } - get("/api/v1/screentime") { - val principal = call.principal() - val userid = - principal?.userId - ?: run { - call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token")) - return@get - } - val remainingSeconds = repository.getScreentime(userid) + authenticate("auth-jwt") { + // 1. Alle verfügbaren Aufgaben abrufen + get("/api/v1/tasks") { + val tasks = repository.getAllTasks() + call.respond(tasks) + } + + // 2. Antwort auf eine Aufgabe überprüfen + post("/api/v1/tasks/verify") { + val request = call.receive() + val principal = call.principal() + val userId = principal?.userId + if (userId == null) { call.respond( - HttpStatusCode.OK, - SyncResponse( - remainingSeconds = remainingSeconds, - isBlocked = remainingSeconds <= 0, + HttpStatusCode.Unauthorized, + mapOf("error" to "Ungültiges oder abgelaufenes Token"), + ) + return@post + } + val taskInfo = repository.getTaskAnswerAndReward(request.taskId) + if (taskInfo == null) { + call.respond( + TaskVerifyResponse( + isCorrect = false, + earnedMinutes = 0, + message = "Aufgabe nicht gefunden!", ), ) + return@post } - // 3. Bildschirmzeit synchronisieren - post("/api/v1/screentime/sync") { - val request = call.receive() - - // Wir holen die echte, fälschungssichere userId aus dem Login-Token: - val principal = call.principal() - val userId = - principal?.userId ?: request.userId // Fallback auf request.userId, falls token noch fehlt - - val currentSeconds = repository.getScreentime(userId) - var newSeconds = currentSeconds - request.usedSecondsSinceLastSync - if (newSeconds < 0) newSeconds = 0 + val (correctAnswer, reward) = taskInfo + val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim() + if (isCorrect) { + val currentScreentime = repository.getScreentime(userId) + val newSeconds = currentScreentime + (reward * 60) repository.updateScreentime(userId, newSeconds) call.respond( - SyncResponse( - remainingSeconds = newSeconds, - isBlocked = newSeconds <= 0, + TaskVerifyResponse( + isCorrect = true, + earnedMinutes = reward, + message = "Super gemacht! Du hast $reward Minuten gewonnen.", + ), + ) + } else { + call.respond( + TaskVerifyResponse( + isCorrect = false, + earnedMinutes = 0, + message = "Schade, das war leider falsch. Versuche es nochmal!", ), ) } } + get("/api/v1/screentime") { + val principal = call.principal() + val userid = + principal?.userId + ?: run { + call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token")) + return@get + } + val remainingSeconds = repository.getScreentime(userid) + call.respond( + HttpStatusCode.OK, + SyncResponse( + remainingSeconds = remainingSeconds, + isBlocked = remainingSeconds <= 0, + ), + ) + } + + // 3. Bildschirmzeit synchronisieren + post("/api/v1/screentime/sync") { + val request = call.receive() + + // Wir holen die echte, fälschungssichere userId aus dem Login-Token: + val principal = call.principal() + val userId = + principal?.userId ?: request.userId // Fallback auf request.userId, falls token noch fehlt + + val currentSeconds = repository.getScreentime(userId) + var newSeconds = currentSeconds - request.usedSecondsSinceLastSync + if (newSeconds < 0) newSeconds = 0 + + repository.updateScreentime(userId, newSeconds) + + call.respond( + SyncResponse( + remainingSeconds = newSeconds, + isBlocked = newSeconds <= 0, + ), + ) + } } } } - diff --git a/src/test/kotlin/ServerTest.kt b/src/test/kotlin/ServerTest.kt index 06ba71c..cce6bf3 100644 --- a/src/test/kotlin/ServerTest.kt +++ b/src/test/kotlin/ServerTest.kt @@ -112,7 +112,7 @@ class ServerTest { setBody( """{ "email": "test@family.de", - "passwordPlain": "strengGeheim1", + "passwordPlain": "StrengGeheim1!", "familyName": "Familie Schmidt", "childAge": 8, "parentPin": "4321" @@ -140,45 +140,48 @@ class ServerTest { } @Test - fun `test registration rejects invalid passwords`() = testApplication { - val jwtService = JwtService() - val repository = InMemoryKidioRepository() + fun `test registration rejects invalid passwords`() = + testApplication { + val jwtService = JwtService() + val repository = InMemoryKidioRepository() - application { - install(ServerContentNegotiation) { - json() + application { + install(ServerContentNegotiation) { + json() + } + configureSecurity(jwtService) + configureRouting(repository, jwtService) } - configureSecurity(jwtService) - configureRouting(repository, jwtService) - } - val invalidPasswords = listOf( - "Kurz1!", - "keingrossbuch1!", - "KeinSonder12", - "Passwort123!😊" - ) + val invalidPasswords = + listOf( + "Kurz1!", + "keingrossbuch1!", + "KeinSonder12", + "Passwort123!😊", + ) - for (password in invalidPasswords) { - val response = client.post("/api/v1/auth/register") { - header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - setBody( - """{ + for (password in invalidPasswords) { + val response = + client.post("/api/v1/auth/register") { + header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + setBody( + """{ "email": "test_${System.currentTimeMillis()}@family.de", "passwordPlain": "$password", "familyName": "Familie Schmidt", "childAge": 8, "parentPin": "4321" - }""" + }""", + ) + } + assertEquals( + HttpStatusCode.BadRequest, + response.status, + "Passwort '$password' hätte abgelehnt werden müssen!", ) } - assertEquals( - HttpStatusCode.BadRequest, - response.status, - "Passwort '$password' hätte abgelehnt werden müssen!" - ) } - } @Test fun `test tasks endpoint and verify with jwt`() =