- password validation
All checks were successful
CI Pipeline / lint-and-test (pull_request) Successful in 2m12s
All checks were successful
CI Pipeline / lint-and-test (pull_request) Successful in 2m12s
- Adding and refactoring tests
This commit is contained in:
parent
aabf94941a
commit
0eb3ce33d0
|
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`() =
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue