From 71f81a97ddbc9af620d62056baca5ecdc53ec1ee Mon Sep 17 00:00:00 2001 From: Oliver Wallisch Date: Mon, 7 Sep 2026 12:37:00 +0200 Subject: [PATCH] add integration of registration and changed datbase schema for single family --- .../backend/data/database/DatabaseFactory.kt | 50 ++++---- .../kidio/backend/domain/models/models.kt | 50 ++++++-- .../domain/repository/KidioRepository.kt | 59 ++++++--- .../oliver/kidio/backend/plugins/Routing.kt | 115 ++++++++++++------ .../oliver/kidio/backend/plugins/Security.kt | 2 +- 5 files changed, 181 insertions(+), 95 deletions(-) diff --git a/src/main/kotlin/com/oliver/kidio/backend/data/database/DatabaseFactory.kt b/src/main/kotlin/com/oliver/kidio/backend/data/database/DatabaseFactory.kt index 3015f1d..d0e50e1 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/data/database/DatabaseFactory.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/data/database/DatabaseFactory.kt @@ -9,7 +9,7 @@ import org.jetbrains.exposed.sql.transactions.transaction object TasksTable : Table("tasks") { val id = varchar("id", 50) val question = text("question") - val options = text("options") // Kommagetrennte Liste der Antwortmöglichkeiten + val options = text("options") val correctAnswer = varchar("correct_answer", 255) val rewardMinutes = integer("reward_minutes") @@ -24,10 +24,15 @@ object ScreentimeTable : Table("user_screentime") { override val primaryKey = PrimaryKey(userId) } +// Option A: Alles Wichtige für den Haushalt & das Kind in einer Tabelle object UserTable : Table("users") { val id = varchar("id", 36) - val username = varchar("username", 50).index(isUnique = true) + val email = varchar("email", 255).uniqueIndex() val passwordHash = varchar("password_hash", 255) + val familyName = varchar("family_name", 100) + val childAge = integer("child_age") // Hier ist das Alter wieder! + val parentPin = varchar("parent_pin", 4) + override val primaryKey = PrimaryKey(id) } @@ -35,26 +40,12 @@ object UserTable : Table("users") { object DatabaseFactory { fun init(config: ApplicationConfig) { val driverClassName = config.propertyOrNull("storage.driverClassName")?.getString() ?: "org.postgresql.Driver" - - val host = System.getenv("DB_HOST") - ?: config.propertyOrNull("storage.host")?.getString() - ?: "localhost" - - val port = System.getenv("DB_PORT") - ?: config.propertyOrNull("storage.port")?.getString() - ?: "5432" - - val dbName = System.getenv("DB_NAME") - ?: config.propertyOrNull("storage.database")?.getString() - ?: "kidio_db" - val dbUsername = System.getenv("DB_USER") - ?: config.propertyOrNull("storage.username")?.getString() - ?: "postgres" - - val password = System.getenv("DB_PASSWORD") - ?: config.propertyOrNull("storage.password")?.getString() - ?: "postgres" + val host = System.getenv("DB_HOST") ?: config.propertyOrNull("storage.host")?.getString() ?: "localhost" + val port = System.getenv("DB_PORT") ?: config.propertyOrNull("storage.port")?.getString() ?: "5432" + val dbName = System.getenv("DB_NAME") ?: config.propertyOrNull("storage.database")?.getString() ?: "kidio_db" + val dbUsername = System.getenv("DB_USER") ?: config.propertyOrNull("storage.username")?.getString() ?: "postgres" + val password = System.getenv("DB_PASSWORD") ?: config.propertyOrNull("storage.password")?.getString() ?: "postgres" val jdbcURL = "jdbc:postgresql://$host:$port/$dbName" @@ -66,9 +57,9 @@ object DatabaseFactory { ) transaction(database) { + // ChildrenTable wurde hier entfernt SchemaUtils.create(TasksTable, ScreentimeTable, UserTable) - // Standard aufgaben, sollte es keine mehr geben if (TasksTable.selectAll().empty()) { TasksTable.insert { it[id] = "t1" @@ -85,8 +76,7 @@ object DatabaseFactory { it[rewardMinutes] = 15 } } - - // Seed a default user screentime if empty + if (ScreentimeTable.selectAll().empty()) { ScreentimeTable.insert { it[userId] = "default_user" @@ -95,13 +85,15 @@ object DatabaseFactory { } } - // Create a standard user if the Users table is empty. + // Korrigierter Seed für UserTable (alle Pflichtfelder befüllt) if (UserTable.selectAll().empty()) { UserTable.insert { - // ID automatische generierung it[id] = "default_user" - it[username] = "default_user" - it[passwordHash] = "1234" + it[email] = "test@kidio.de" + it[passwordHash] = "1234" // Dummy-Hash für den Start + it[familyName] = "Familie Test" + it[childAge] = 8 + it[parentPin] = "1234" } } } @@ -109,4 +101,4 @@ object DatabaseFactory { suspend fun dbQuery(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/oliver/kidio/backend/domain/models/models.kt b/src/main/kotlin/com/oliver/kidio/backend/domain/models/models.kt index 9eef0ef..bda43d0 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/domain/models/models.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/domain/models/models.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable data class Task( val id: String, val question: String, - val options: List, // Antwortmöglichkeiten + val options: List, val rewardMinutes: Int // Wie viel Belohnung es gibt ) @@ -33,20 +33,44 @@ data class SyncRequest( val usedSecondsSinceLastSync: Int ) -@Serializable -data class LoginRequest( - val userId: String -) - -@Serializable -data class User( - val id: String, - val username: String, - val passwordHash: String -) - @Serializable data class SyncResponse( val remainingSeconds: Int, val isBlocked: Boolean ) + +// --- NEU: AUTHENTIFIZIERUNG --- + +@Serializable +data class RegisterRequest( + val email: String, + val passwordPlain: String, + val familyName: String, + val childAge: Int, + val parentPin: String +) + +@Serializable +data class LoginRequest( + val userId: String, + val email: String, + val passwordPlain: String +) + +@Serializable +data class AuthResponse( + val token: String, // Der digitale "Ausweis" (JWT) + val userId: String, // Wichtig für die App, um Aufgaben zuzuordnen + val familyName: String, // Für "Hallo Familie Müller!" + val childAge: Int +) + +// Interne Backend-Klasse (Domain Model). +data class User( + val id: String, + val email: String, + val passwordHash: String, + val familyName: String, + val childAge: Int, + val parentPin: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/oliver/kidio/backend/domain/repository/KidioRepository.kt b/src/main/kotlin/com/oliver/kidio/backend/domain/repository/KidioRepository.kt index 277ccb6..4b9d645 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/domain/repository/KidioRepository.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/domain/repository/KidioRepository.kt @@ -3,20 +3,22 @@ package com.oliver.kidio.backend.domain.repository import com.oliver.kidio.backend.Task import com.oliver.kidio.backend.User import com.oliver.kidio.backend.data.database.DatabaseFactory +import com.oliver.kidio.backend.data.database.DatabaseFactory.dbQuery import com.oliver.kidio.backend.data.database.RedisFactory import com.oliver.kidio.backend.data.database.ScreentimeTable import com.oliver.kidio.backend.data.database.TasksTable import com.oliver.kidio.backend.data.database.UserTable import org.jetbrains.exposed.sql.* +import org.mindrot.jbcrypt.BCrypt interface KidioRepository { suspend fun getAllTasks(): List suspend fun getTaskAnswerAndReward(taskId: String): Pair? suspend fun getScreentime(userId: String): Int suspend fun updateScreentime(userId: String, seconds: Int) - - suspend fun findByUsername(username: String): User? + suspend fun findByEmail(username: String): User? suspend fun verifyPassword(password: String, passwordHash: String): Boolean + suspend fun registerUser(user: User): Boolean } class ExposedKidioRepository : KidioRepository { @@ -31,6 +33,23 @@ class ExposedKidioRepository : KidioRepository { } } + override suspend fun registerUser(user: User): Boolean = dbQuery { + UserTable.insert { + it[UserTable.id] = user.id + it[UserTable.email] = user.email + it[UserTable.passwordHash] = user.passwordHash + it[UserTable.familyName] = user.familyName + it[UserTable.childAge] = user.childAge + it[UserTable.parentPin] = user.parentPin + } + ScreentimeTable.insert { + it[ScreentimeTable.userId] = user.id + it[ScreentimeTable.remainingSeconds] = 1800 + it[ScreentimeTable.parentPin] = user.parentPin + } + true + } + override suspend fun getTaskAnswerAndReward(taskId: String): Pair? = DatabaseFactory.dbQuery { TasksTable.selectAll() .where { TasksTable.id eq taskId } @@ -52,12 +71,14 @@ class ExposedKidioRepository : KidioRepository { } catch (e: Exception) { println("WARNUNG: Konnte nicht aus Redis lesen (Postgres-Fallback genutzt): ${e.message}") } + val dbSeconds = DatabaseFactory.dbQuery { ScreentimeTable.selectAll() .where { ScreentimeTable.userId eq userId } .map { it[ScreentimeTable.remainingSeconds] } .singleOrNull() ?: 1800 } + try { RedisFactory.getResource().use { jedis -> jedis.setex(redisKey, 3600, dbSeconds.toString()) @@ -69,7 +90,7 @@ class ExposedKidioRepository : KidioRepository { return dbSeconds } - override suspend fun updateScreentime(userId: String, seconds: Int): Unit = DatabaseFactory.dbQuery { + override suspend fun updateScreentime(userId: String, seconds: Int) = DatabaseFactory.dbQuery { val rowsUpdated = ScreentimeTable.update({ ScreentimeTable.userId eq userId }) { it[remainingSeconds] = seconds } @@ -78,25 +99,29 @@ class ExposedKidioRepository : KidioRepository { it[ScreentimeTable.userId] = userId it[remainingSeconds] = seconds it[parentPin] = "1234" + } } } - override suspend fun findByUsername(username: String): User? = DatabaseFactory.dbQuery { + override suspend fun findByEmail(username: String): User? = DatabaseFactory.dbQuery { UserTable.selectAll() - .where { UserTable.username eq username } + .where { UserTable.email eq username } .map { User( id = it[UserTable.id], - username = it[UserTable.username], - passwordHash = it[UserTable.passwordHash] + email = it[UserTable.email], + passwordHash = it[UserTable.passwordHash], + familyName = it[UserTable.familyName], + childAge = it[UserTable.childAge], + parentPin = it[UserTable.parentPin] ) } .singleOrNull() } override suspend fun verifyPassword(password: String, passwordHash: String): Boolean { - return password == passwordHash + return BCrypt.checkpw(password, passwordHash) } } @@ -112,10 +137,7 @@ class InMemoryKidioRepository : KidioRepository { private val screentimes = mutableMapOf( "default_user" to 1800 ) - - private val users = mutableListOf( - User(id = "1", username = "max", passwordHash = "geheim123") - ) + private val users = mutableListOf() override suspend fun getAllTasks(): List = tasks @@ -127,11 +149,16 @@ class InMemoryKidioRepository : KidioRepository { screentimes[userId] = seconds } - override suspend fun findByUsername(username: String): User? { - return users.find { it.username == username } + override suspend fun findByEmail(username: String): User? { + return users.find { it.email == username } } override suspend fun verifyPassword(password: String, passwordHash: String): Boolean { - return password == passwordHash + TODO("Not yet implemented") } -} + + override suspend fun registerUser(user: User): Boolean { + users.add(user) + return true + } +} \ No newline at end of file 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 eddad2f..b100f5a 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/plugins/Routing.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/plugins/Routing.kt @@ -3,15 +3,53 @@ package com.oliver.kidio.backend.plugins import com.oliver.kidio.backend.* import com.oliver.kidio.backend.domain.repository.KidioRepository import com.oliver.kidio.backend.security.JwtService +import io.ktor.http.HttpStatusCode import io.ktor.server.application.* import io.ktor.server.auth.authenticate +import io.ktor.server.auth.jwt.JWTPrincipal +import io.ktor.server.auth.principal import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import org.mindrot.jbcrypt.BCrypt +import java.util.UUID 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 (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 get("/api/v1/health") { call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!")) @@ -33,47 +71,52 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe } // 2. Antwort auf eine Aufgabe überprüfen - post("/api/v1/tasks/verify") { - val request = call.receive() + post("/api/v1/tasks/verify") { + val request = call.receive() + val principal = call.principal() + val userId = principal?.payload?.getClaim("userId")?.asString() - val taskInfo = repository.getTaskAnswerAndReward(request.taskId) - - if (taskInfo == null) { - call.respond( - TaskVerifyResponse( - isCorrect = false, - earnedMinutes = 0, - message = "Aufgabe nicht gefunden!" - ) - ) + if (userId == null){ + call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiges oder abgelaufenes Token")) return@post - } - - val (correctAnswer, reward) = taskInfo - val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim() - - if (isCorrect) { - val currentScreentime = repository.getScreentime("default_user") - val newSeconds = currentScreentime + (reward * 60) - repository.updateScreentime("default_user", newSeconds) - - call.respond( - TaskVerifyResponse( - isCorrect = true, - earnedMinutes = reward, - message = "Super gemacht! Du hast $reward Minuten gewonnen." + } + val taskInfo = repository.getTaskAnswerAndReward(request.taskId) + if (taskInfo == null) { + call.respond( + TaskVerifyResponse( + isCorrect = false, + earnedMinutes = 0, + message = "Aufgabe nicht gefunden!" + ) ) - ) - } else { - call.respond( - TaskVerifyResponse( - isCorrect = false, - earnedMinutes = 0, - message = "Schade, das war leider falsch. Versuche es nochmal!" + return@post + } + + 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( + 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!" + ) + ) + } } - } // 3. Bildschirmzeit synchronisieren post("/api/v1/screentime/sync") { diff --git a/src/main/kotlin/com/oliver/kidio/backend/plugins/Security.kt b/src/main/kotlin/com/oliver/kidio/backend/plugins/Security.kt index cd05f67..ec9244b 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/plugins/Security.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/plugins/Security.kt @@ -8,7 +8,7 @@ import io.ktor.server.auth.jwt.* /** * Repräsentiert den authentifizierten Benutzer im Request-Kontext. */ -data class UserPrincipal(val userId: String) : Principal +data class UserPrincipal(val userId: String) fun Application.configureSecurity(jwtService: JwtService) { install(Authentication) { -- 2.43.7