- password validation
All checks were successful
CI Pipeline / lint-and-test (pull_request) Successful in 2m12s

- Adding and refactoring tests
This commit is contained in:
aldemirm 2026-09-14 12:10:33 +02:00
parent aabf94941a
commit 0eb3ce33d0
2 changed files with 193 additions and 194 deletions

View file

@ -17,207 +17,203 @@ fun isValidPassword(password: String): Boolean {
if (password.length < 8) return false if (password.length < 8) return false
val hasUppercase = password.any { it.isUpperCase() } val hasUppercase = password.any { it.isUpperCase() }
val hasEmoji = password.any { char -> val hasEmoji =
val type = Character.getType(char).toByte() password.any { char ->
type == Character.SURROGATE || val type = Character.getType(char).toByte()
type == Character.SURROGATE ||
type == Character.OTHER_SYMBOL || type == Character.OTHER_SYMBOL ||
char.code in 0x1F600..0x1F64F || char.code in 0x1F600..0x1F64F ||
char.code in 0x1F300..0x1F5FF || char.code in 0x1F300..0x1F5FF ||
char.code in 0x1F680..0x1F6FF || char.code in 0x1F680..0x1F6FF ||
char.code in 0x2600..0x26FF char.code in 0x2600..0x26FF
} }
val hasSpecialChar = password.any { !it.isLetterOrDigit() && !hasEmoji } val hasSpecialChar = password.any { !it.isLetterOrDigit() && !hasEmoji }
return hasUppercase && hasSpecialChar && !hasEmoji return hasUppercase && hasSpecialChar && !hasEmoji
} }
fun Application.configureRouting(repository: KidioRepository, jwtService : JwtService) { fun Application.configureRouting(
fun Application.configureRouting( repository: KidioRepository,
repository: KidioRepository, jwtService: JwtService,
jwtService: JwtService, ) {
) { routing {
routing { post("/api/v1/auth/register") {
post("/api/v1/auth/register") { val registerRequest = call.receive<RegisterRequest>()
val registerRequest = call.receive<RegisterRequest>() if (registerRequest.email.isBlank() || registerRequest.passwordPlain.isBlank()) {
if (registerRequest.email.isBlank() || registerRequest.passwordPlain.isBlank()) { call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus") return@post
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",
),
)
} }
// Health-Check if (!isValidPassword(registerRequest.passwordPlain)) {
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<LoginRequest>()
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( call.respond(
HttpStatusCode.OK, HttpStatusCode.BadRequest,
AuthResponse( "Das Passwort muss mindestens 8 Zeichen lang sein, " +
token, "mindestens einen Großbuchstaben und ein Sonderzeichen enthalten.",
userId = getUser.id,
familyName = getUser.familyName,
childAge = getUser.childAge,
),
) )
return@post 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 // datenbank insert hinzufügen
get("/api/v1/tasks") { repository.registerUser(
val tasks = repository.getAllTasks() User(
call.respond(tasks) userId,
} registerRequest.email,
passwordHash,
registerRequest.familyName,
registerRequest.childAge,
registerRequest.parentPin,
),
)
// 2. Antwort auf eine Aufgabe überprüfen val token = jwtService.generateToken(userId)
post("/api/v1/tasks/verify") {
val request = call.receive<TaskVerifyRequest>()
val principal = call.principal<UserPrincipal>()
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 (correctAnswer, reward) = taskInfo call.respond(
val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim() 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) { post("/api/v1/auth/login") {
val currentScreentime = repository.getScreentime(userId) val loginRequest = call.receive<LoginRequest>()
val newSeconds = currentScreentime + (reward * 60) if (loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()) {
repository.updateScreentime(userId, newSeconds) 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( authenticate("auth-jwt") {
TaskVerifyResponse( // 1. Alle verfügbaren Aufgaben abrufen
isCorrect = true, get("/api/v1/tasks") {
earnedMinutes = reward, val tasks = repository.getAllTasks()
message = "Super gemacht! Du hast $reward Minuten gewonnen.", call.respond(tasks)
), }
)
} else { // 2. Antwort auf eine Aufgabe überprüfen
call.respond( post("/api/v1/tasks/verify") {
TaskVerifyResponse( val request = call.receive<TaskVerifyRequest>()
isCorrect = false, val principal = call.principal<UserPrincipal>()
earnedMinutes = 0, val userId = principal?.userId
message = "Schade, das war leider falsch. Versuche es nochmal!", if (userId == null) {
),
)
}
}
get("/api/v1/screentime") {
val principal = call.principal<UserPrincipal>()
val userid =
principal?.userId
?: run {
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token"))
return@get
}
val remainingSeconds = repository.getScreentime(userid)
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.Unauthorized,
SyncResponse( mapOf("error" to "Ungültiges oder abgelaufenes Token"),
remainingSeconds = remainingSeconds, )
isBlocked = remainingSeconds <= 0, 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 val (correctAnswer, reward) = taskInfo
post("/api/v1/screentime/sync") { val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim()
val request = call.receive<SyncRequest>()
// Wir holen die echte, fälschungssichere userId aus dem Login-Token:
val principal = call.principal<UserPrincipal>()
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
if (isCorrect) {
val currentScreentime = repository.getScreentime(userId)
val newSeconds = currentScreentime + (reward * 60)
repository.updateScreentime(userId, newSeconds) repository.updateScreentime(userId, newSeconds)
call.respond( call.respond(
SyncResponse( TaskVerifyResponse(
remainingSeconds = newSeconds, isCorrect = true,
isBlocked = newSeconds <= 0, 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<UserPrincipal>()
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<SyncRequest>()
// Wir holen die echte, fälschungssichere userId aus dem Login-Token:
val principal = call.principal<UserPrincipal>()
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,
),
)
}
} }
} }
} }

View file

@ -112,7 +112,7 @@ class ServerTest {
setBody( setBody(
"""{ """{
"email": "test@family.de", "email": "test@family.de",
"passwordPlain": "strengGeheim1", "passwordPlain": "StrengGeheim1!",
"familyName": "Familie Schmidt", "familyName": "Familie Schmidt",
"childAge": 8, "childAge": 8,
"parentPin": "4321" "parentPin": "4321"
@ -140,45 +140,48 @@ class ServerTest {
} }
@Test @Test
fun `test registration rejects invalid passwords`() = testApplication { fun `test registration rejects invalid passwords`() =
val jwtService = JwtService() testApplication {
val repository = InMemoryKidioRepository() val jwtService = JwtService()
val repository = InMemoryKidioRepository()
application { application {
install(ServerContentNegotiation) { install(ServerContentNegotiation) {
json() json()
}
configureSecurity(jwtService)
configureRouting(repository, jwtService)
} }
configureSecurity(jwtService)
configureRouting(repository, jwtService)
}
val invalidPasswords = listOf( val invalidPasswords =
"Kurz1!", listOf(
"keingrossbuch1!", "Kurz1!",
"KeinSonder12", "keingrossbuch1!",
"Passwort123!😊" "KeinSonder12",
) "Passwort123!😊",
)
for (password in invalidPasswords) { for (password in invalidPasswords) {
val response = client.post("/api/v1/auth/register") { val response =
header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) client.post("/api/v1/auth/register") {
setBody( header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
"""{ setBody(
"""{
"email": "test_${System.currentTimeMillis()}@family.de", "email": "test_${System.currentTimeMillis()}@family.de",
"passwordPlain": "$password", "passwordPlain": "$password",
"familyName": "Familie Schmidt", "familyName": "Familie Schmidt",
"childAge": 8, "childAge": 8,
"parentPin": "4321" "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 @Test
fun `test tasks endpoint and verify with jwt`() = fun `test tasks endpoint and verify with jwt`() =