From 49c472f7483937199fdbf8405cbc948e729af8ab Mon Sep 17 00:00:00 2001 From: Oliver Wallisch Date: Tue, 8 Sep 2026 10:43:12 +0200 Subject: [PATCH] integreating Register and Login Endpoint --- README.md | 176 ++++++++++++++++-- build.gradle.kts | 3 + src/.DS_Store | Bin 0 -> 6148 bytes src/main/.DS_Store | Bin 0 -> 6148 bytes .../backend/data/database/DatabaseFactory.kt | 10 +- .../kidio/backend/domain/models/models.kt | 1 - .../domain/repository/KidioRepository.kt | 52 +++--- .../oliver/kidio/backend/plugins/Routing.kt | 22 ++- 8 files changed, 209 insertions(+), 55 deletions(-) create mode 100644 src/.DS_Store create mode 100644 src/main/.DS_Store diff --git a/README.md b/README.md index 44192e7..3a73713 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,171 @@ -# Kidio_backend +# Kidio Backend -This project was created using the [Ktor Project Generator](https://start.ktor.io). +Das Kidio-Backend ist eine gamifizierte Lösung zur Verwaltung von Bildschirmzeit für Kinder. Die Anwendung basiert auf dem **Ktor**-Framework in Kotlin und kombiniert **PostgreSQL** für die Persistenz mit **Redis** als Caching-Layer für schnelle Zugriffszeiten. -Here are some useful links to get you started: +Kinder können durch das Lösen von Lernaufgaben (z. B. Mathe-Aufgaben) zusätzliche Bildschirmzeit erwerben, während das System ihre verbrauchte Bildschirmzeit synchronisiert und bei Ablauf sperrt. -* [Ktor Documentation](https://ktor.io/docs/home.html) -* [Ktor GitHub page](https://github.com/ktorio/ktor) -* [Ktor Slack chat](https://app.slack.com/client/T09229ZC6/C0A974TJ9). [Request an invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up). +--- ## Features -Here's a list of features included in this project: +Hier ist eine Übersicht der implementierten Kernfunktionen des Kidio-Backends: -| Name | Description | +| Name | Beschreibung | |------|-------------| +| **JWT Authentifizierung** | Sichere API-Endpunkte für authentifizierte Benutzer über JSON Web Tokens (JWT). | +| **Lernaufgaben (Tasks)** | Abrufen von interaktiven Lernaufgaben mit vordefinierten Antwortmöglichkeiten und Belohnungen (z. B. Minuten). | +| **Antwort-Verifizierung** | Überprüfung der vom Kind ausgewählten Antworten. Bei korrekter Antwort wird die Belohnung direkt auf die verbleibende Bildschirmzeit aufgerechnet. | +| **Bildschirmzeit-Synchronisation** | Laufender Datenabgleich der genutzten Bildschirmzeit. Sinkt die Zeit auf oder unter Null, wird der Status automatisch auf `isBlocked = true` gesetzt. | +| **Zwei-Ebenen-Speicher** | Datenhaltung in **PostgreSQL** (via JetBrains Exposed ORM) kombiniert mit **Redis-Caching** (via Jedis) für optimale Performance bei der Screentime-Abfrage. | +| **Fallback-Mechanismus** | Ist die Redis-Instanz nicht erreichbar, fällt das System automatisch und transparent auf PostgreSQL zurück. | -## Building & Running +--- -To build or run the project, use one of the following tasks: +## Technologiestack -| Task | Description | -|-------------------|-------------------| -| `./gradlew test` | Run the tests | -| `./gradlew build` | Build the project | -| `./gradlew run` | Run the server | +- **Framework**: [Ktor](https://ktor.io/) (Kotlin-basiertes asynchrones Web-Framework) +- **Datenbank-ORM**: [JetBrains Exposed](https://github.com/JetBrains/Exposed) +- **Persistenz**: PostgreSQL +- **Caching**: Redis (via Jedis Client) +- **Sicherheit & Auth**: JWT (Java JWT von auth0) +- **Serialization**: Kotlinx Serialization (JSON) +- **Testing**: Ktor Server Testing, JUnit -If the server starts successfully, you'll see the following output: +--- +## API-Endpunkte + +Alle Endpunkte befinden sich unter dem Präfix `/api/v1`. + +### Öffentliche Endpunkte + +#### 1. Health-Check +- **Methode**: `GET` +- **Pfad**: `/api/v1/health` +- **Beschreibung**: Prüft, ob der Backend-Server ordnungsgemäß läuft. +- **Antwort**: + ```json + { + "status": "OK", + "message": "Kidio Backend läuft!" + } + ``` + +#### 2. Benutzer-Login (Auth-Token abholen) +- **Methode**: `POST` +- **Pfad**: `/api/v1/auth/login` +- **Request-Body**: + ```json + { + "userId": "default_user" + } + ``` +- **Antwort**: + ```json + { + "token": "eyJhbGciOiJIUzI1NiIsIn..." + } + ``` + +### Geschützte Endpunkte (Erfordern JWT im `Authorization: Bearer ` Header) + +#### 3. Alle verfügbaren Aufgaben abrufen +- **Methode**: `GET` +- **Pfad**: `/api/v1/tasks` +- **Beschreibung**: Liefert eine Liste aller verfügbaren Lernaufgaben. +- **Antwort**: + ```json + [ + { + "id": "t1", + "question": "Was ist 12 + 15?", + "options": ["25", "27", "30", "22"], + "rewardMinutes": 10 + } + ] + ``` + +#### 4. Antwort überprüfen & Belohnung verbuchen +- **Methode**: `POST` +- **Pfad**: `/api/v1/tasks/verify` +- **Request-Body**: + ```json + { + "taskId": "t1", + "selectedAnswer": "27" + } + ``` +- **Antwort (Erfolg)**: + ```json + { + "isCorrect": true, + "earnedMinutes": 10, + "message": "Super gemacht! Du hast 10 Minuten gewonnen." + } + ``` +- **Antwort (Fehler)**: + ```json + { + "isCorrect": false, + "earnedMinutes": 0, + "message": "Schade, das war leider falsch. Versuche es nochmal!" + } + ``` + +#### 5. Bildschirmzeit synchronisieren +- **Methode**: `POST` +- **Pfad**: `/api/v1/screentime/sync` +- **Beschreibung**: Zieht die seit dem letzten Abgleich verbrauchten Sekunden von der verfügbaren Bildschirmzeit ab. +- **Request-Body**: + ```json + { + "userId": "default_user", + "usedSecondsSinceLastSync": 200 + } + ``` +- **Antwort**: + ```json + { + "remainingSeconds": 1600, + "isBlocked": false + } + ``` + +--- + +## Projektkonfiguration und Umgebungsvariablen + +Das Projekt wird über die Datei `src/main/resources/application.yaml` konfiguriert. Folgende Umgebungsvariablen können zur flexiblen Docker- oder Deployment-Konfiguration übergeben werden: + +| Variable | Beschreibung | Standardwert | +|----------|--------------|--------------| +| `DB_HOST` | Hostname der PostgreSQL-Datenbank | `localhost` / aus `application.yaml` | +| `DB_PORT` | Port der PostgreSQL-Datenbank | `5432` / aus `application.yaml` | +| `DB_NAME` | Name der PostgreSQL-Datenbank | `kidio_db` | +| `DB_USER` | PostgreSQL-Benutzername | `kidio_admin` | +| `DB_PASSWORD` | PostgreSQL-Passwort | `learning` | +| `REDIS_HOST` | Hostname der Redis-Instanz | `localhost` | +| `REDIS_PORT` | Port der Redis-Instanz | `6379` | +| `JWT_SECRET` | Geheimer Schlüssel zur Signierung der JWTs | `super-geheimes-secret-fuer-dev` | + +--- + +## Lokale Entwicklung, Bauen & Testen + +Zur Ausführung des Projekts werden die folgenden Gradle-Tasks bereitgestellt: + +| Task | Beschreibung | +|------|-------------| +| `./gradlew test` | Führt alle automatisierten Unit- und Integrationstests aus (nutzt die InMemory-Repository-Implementierung). | +| `./gradlew build` | Kompiliert das Projekt und baut das Artefakt. | +| `./gradlew run` | Startet den Ktor-Entwicklungsserver lokal auf Port `8080`. | + +```bash +# Tests ausführen +./gradlew test + +# Server starten +./gradlew run ``` -2024-12-04 14:32:45.584 [main] INFO Application - Application started in 0.303 seconds. -2024-12-04 14:32:45.682 [main] INFO Application - Responding at http://0.0.0.0:8080 -``` + +Sobald der Server erfolgreich gestartet ist, ist er unter `http://localhost:8080` erreichbar. diff --git a/build.gradle.kts b/build.gradle.kts index 776f637..594bcaa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,9 @@ dependencies { implementation(libs.logback.classic) + // Bcrypt + implementation("org.mindrot:jbcrypt:0.4") + // JSON-Unterstützung und Serialization implementation("io.ktor:ktor-server-content-negotiation-jvm") implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7b0d36729e2ee777a660f9e8c6709dd97bc2fb68 GIT binary patch literal 6148 zcmeH~F>V4u3`M`g7D#EfOgRk)$PGpaPQV2S&>%r5BKkQx-ySzvsH0W%E!l7ES!;KH zv9ktX>-*^w7y&HlPOLmk%$N_j;tOYdpMH*)!|itQBJHgMp3+B5_H$d10#ZNMM4T9irw zDe%t}uwk>?toc%Twm!X{*Y{cVb)%DUIm54?049DEf6&9YUwlEql)lcSuRDcTnbp`DEP~e6&aSHTL2L^8efDOWKn0qe) zEEWLP#3>LFmHE{}bI^s?T@@K$wp;3X~R^SfH*%hGx literal 0 HcmV?d00001 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 d0e50e1..df3362f 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 @@ -19,18 +19,15 @@ object TasksTable : Table("tasks") { object ScreentimeTable : Table("user_screentime") { val userId = varchar("user_id", 50) val remainingSeconds = integer("remaining_seconds") - val parentPin = varchar("parent_pin", 10) 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 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 childAge = integer("child_age") val parentPin = varchar("parent_pin", 4) @@ -57,7 +54,6 @@ object DatabaseFactory { ) transaction(database) { - // ChildrenTable wurde hier entfernt SchemaUtils.create(TasksTable, ScreentimeTable, UserTable) if (TasksTable.selectAll().empty()) { @@ -81,11 +77,9 @@ object DatabaseFactory { ScreentimeTable.insert { it[userId] = "default_user" it[remainingSeconds] = 1800 - it[parentPin] = "1234" } } - - // Korrigierter Seed für UserTable (alle Pflichtfelder befüllt) + if (UserTable.selectAll().empty()) { UserTable.insert { it[id] = "default_user" 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 bda43d0..79bccbf 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 @@ -52,7 +52,6 @@ data class RegisterRequest( @Serializable data class LoginRequest( - val userId: String, val email: String, val passwordPlain: String ) 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 4b9d645..e989fb1 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,7 +3,6 @@ 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 @@ -16,7 +15,7 @@ interface KidioRepository { suspend fun getTaskAnswerAndReward(taskId: String): Pair? suspend fun getScreentime(userId: String): Int suspend fun updateScreentime(userId: String, seconds: Int) - suspend fun findByEmail(username: String): User? + suspend fun findByEmail(email: String): User? suspend fun verifyPassword(password: String, passwordHash: String): Boolean suspend fun registerUser(user: User): Boolean } @@ -33,21 +32,25 @@ 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 + override suspend fun registerUser(user: User): Boolean = DatabaseFactory.dbQuery { + try { + UserTable.insert { + it[id] = user.id + it[email] = user.email + it[passwordHash] = user.passwordHash + it[familyName] = user.familyName + it[childAge] = user.childAge + it[parentPin] = user.parentPin + } + ScreentimeTable.insert { + it[userId] = user.id + it[remainingSeconds] = 1800 // Startguthaben: 30 Minuten + } + true + } catch (e: Exception) { + println("FEHLER bei Registrierung: ${e.message}") + false } - 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 { @@ -75,7 +78,7 @@ class ExposedKidioRepository : KidioRepository { val dbSeconds = DatabaseFactory.dbQuery { ScreentimeTable.selectAll() .where { ScreentimeTable.userId eq userId } - .map { it[ScreentimeTable.remainingSeconds] } + .map { it[ScreentimeTable.remainingSeconds] } // Korrigiert: Abfrage der Sekunden! .singleOrNull() ?: 1800 } @@ -97,16 +100,14 @@ class ExposedKidioRepository : KidioRepository { if (rowsUpdated == 0) { ScreentimeTable.insert { it[ScreentimeTable.userId] = userId - it[remainingSeconds] = seconds - it[parentPin] = "1234" - + it[ScreentimeTable.remainingSeconds] = seconds // Korrigiert: Wert wird wieder mitgegeben! } } } - override suspend fun findByEmail(username: String): User? = DatabaseFactory.dbQuery { + override suspend fun findByEmail(email: String): User? = DatabaseFactory.dbQuery { UserTable.selectAll() - .where { UserTable.email eq username } + .where { UserTable.email eq email } .map { User( id = it[UserTable.id], @@ -149,16 +150,17 @@ class InMemoryKidioRepository : KidioRepository { screentimes[userId] = seconds } - override suspend fun findByEmail(username: String): User? { - return users.find { it.email == username } + override suspend fun findByEmail(email: String): User? { + return users.find { it.email == email } } override suspend fun verifyPassword(password: String, passwordHash: String): Boolean { - TODO("Not yet implemented") + return BCrypt.checkpw(password, passwordHash) } override suspend fun registerUser(user: User): Boolean { users.add(user) + screentimes[user.id] = 1800 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 b100f5a..0ac4953 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/plugins/Routing.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/plugins/Routing.kt @@ -57,9 +57,27 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe 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 + + + - val token = jwtService.generateToken(userId = loginRequest.userId) - call.respond(mapOf("token" to token)) } authenticate("auth-jwt") {