Compare commits

..

4 commits

28 changed files with 365 additions and 124 deletions

1
.gitignore vendored
View file

@ -11,4 +11,3 @@ build/
*.iws *.iws
*.ipr *.ipr
out/ out/
.gradle/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

Binary file not shown.

View file

@ -0,0 +1,2 @@
#Wed Sep 02 15:03:29 CEST 2026
gradle.version=9.5.1

Binary file not shown.

BIN
.gradle/file-system.probe Normal file

Binary file not shown.

View file

174
README.md
View file

@ -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 ## 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 | - **Framework**: [Ktor](https://ktor.io/) (Kotlin-basiertes asynchrones Web-Framework)
|-------------------|-------------------| - **Datenbank-ORM**: [JetBrains Exposed](https://github.com/JetBrains/Exposed)
| `./gradlew test` | Run the tests | - **Persistenz**: PostgreSQL
| `./gradlew build` | Build the project | - **Caching**: Redis (via Jedis Client)
| `./gradlew run` | Run the server | - **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!"
}
``` ```
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 #### 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 <Token>` 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
```
Sobald der Server erfolgreich gestartet ist, ist er unter `http://localhost:8080` erreichbar.

View file

@ -26,6 +26,9 @@ dependencies {
implementation(libs.logback.classic) implementation(libs.logback.classic)
// Bcrypt
implementation("org.mindrot:jbcrypt:0.4")
// JSON-Unterstützung und Serialization // JSON-Unterstützung und Serialization
implementation("io.ktor:ktor-server-content-negotiation-jvm") implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/main/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -9,7 +9,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
object TasksTable : Table("tasks") { object TasksTable : Table("tasks") {
val id = varchar("id", 50) val id = varchar("id", 50)
val question = text("question") val question = text("question")
val options = text("options") // Kommagetrennte Liste der Antwortmöglichkeiten val options = text("options")
val correctAnswer = varchar("correct_answer", 255) val correctAnswer = varchar("correct_answer", 255)
val rewardMinutes = integer("reward_minutes") val rewardMinutes = integer("reward_minutes")
@ -19,15 +19,17 @@ object TasksTable : Table("tasks") {
object ScreentimeTable : Table("user_screentime") { object ScreentimeTable : Table("user_screentime") {
val userId = varchar("user_id", 50) val userId = varchar("user_id", 50)
val remainingSeconds = integer("remaining_seconds") val remainingSeconds = integer("remaining_seconds")
val parentPin = varchar("parent_pin", 10)
override val primaryKey = PrimaryKey(userId) override val primaryKey = PrimaryKey(userId)
} }
object UserTable : Table("users") { object UserTable : Table("users") {
val id = varchar("id", 36) 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 passwordHash = varchar("password_hash", 255)
val familyName = varchar("family_name", 100)
val childAge = integer("child_age")
val parentPin = varchar("parent_pin", 4)
override val primaryKey = PrimaryKey(id) override val primaryKey = PrimaryKey(id)
} }
@ -36,25 +38,11 @@ object DatabaseFactory {
fun init(config: ApplicationConfig) { fun init(config: ApplicationConfig) {
val driverClassName = config.propertyOrNull("storage.driverClassName")?.getString() ?: "org.postgresql.Driver" val driverClassName = config.propertyOrNull("storage.driverClassName")?.getString() ?: "org.postgresql.Driver"
val host = System.getenv("DB_HOST") val host = System.getenv("DB_HOST") ?: config.propertyOrNull("storage.host")?.getString() ?: "localhost"
?: config.propertyOrNull("storage.host")?.getString() val port = System.getenv("DB_PORT") ?: config.propertyOrNull("storage.port")?.getString() ?: "5432"
?: "localhost" 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 port = System.getenv("DB_PORT") val password = System.getenv("DB_PASSWORD") ?: config.propertyOrNull("storage.password")?.getString() ?: "postgres"
?: 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" val jdbcURL = "jdbc:postgresql://$host:$port/$dbName"
@ -68,7 +56,6 @@ object DatabaseFactory {
transaction(database) { transaction(database) {
SchemaUtils.create(TasksTable, ScreentimeTable, UserTable) SchemaUtils.create(TasksTable, ScreentimeTable, UserTable)
// Standard aufgaben, sollte es keine mehr geben
if (TasksTable.selectAll().empty()) { if (TasksTable.selectAll().empty()) {
TasksTable.insert { TasksTable.insert {
it[id] = "t1" it[id] = "t1"
@ -86,22 +73,21 @@ object DatabaseFactory {
} }
} }
// Seed a default user screentime if empty
if (ScreentimeTable.selectAll().empty()) { if (ScreentimeTable.selectAll().empty()) {
ScreentimeTable.insert { ScreentimeTable.insert {
it[userId] = "default_user" it[userId] = "default_user"
it[remainingSeconds] = 1800 it[remainingSeconds] = 1800
it[parentPin] = "1234"
} }
} }
// Create a standard user if the Users table is empty.
if (UserTable.selectAll().empty()) { if (UserTable.selectAll().empty()) {
UserTable.insert { UserTable.insert {
// ID automatische generierung
it[id] = "default_user" it[id] = "default_user"
it[username] = "default_user" it[email] = "test@kidio.de"
it[passwordHash] = "1234" it[passwordHash] = "1234" // Dummy-Hash für den Start
it[familyName] = "Familie Test"
it[childAge] = 8
it[parentPin] = "1234"
} }
} }
} }

View file

@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
data class Task( data class Task(
val id: String, val id: String,
val question: String, val question: String,
val options: List<String>, // Antwortmöglichkeiten val options: List<String>,
val rewardMinutes: Int // Wie viel Belohnung es gibt val rewardMinutes: Int // Wie viel Belohnung es gibt
) )
@ -33,20 +33,43 @@ data class SyncRequest(
val usedSecondsSinceLastSync: Int val usedSecondsSinceLastSync: Int
) )
@Serializable
data class LoginRequest(
val userId: String
)
@Serializable
data class User(
val id: String,
val username: String,
val passwordHash: String
)
@Serializable @Serializable
data class SyncResponse( data class SyncResponse(
val remainingSeconds: Int, val remainingSeconds: Int,
val isBlocked: Boolean 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 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
)

View file

@ -8,15 +8,16 @@ import com.oliver.kidio.backend.data.database.ScreentimeTable
import com.oliver.kidio.backend.data.database.TasksTable import com.oliver.kidio.backend.data.database.TasksTable
import com.oliver.kidio.backend.data.database.UserTable import com.oliver.kidio.backend.data.database.UserTable
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.mindrot.jbcrypt.BCrypt
interface KidioRepository { interface KidioRepository {
suspend fun getAllTasks(): List<Task> suspend fun getAllTasks(): List<Task>
suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>?
suspend fun getScreentime(userId: String): Int suspend fun getScreentime(userId: String): Int
suspend fun updateScreentime(userId: String, seconds: Int) suspend fun updateScreentime(userId: String, seconds: Int)
suspend fun findByEmail(email: String): User?
suspend fun findByUsername(username: String): User?
suspend fun verifyPassword(password: String, passwordHash: String): Boolean suspend fun verifyPassword(password: String, passwordHash: String): Boolean
suspend fun registerUser(user: User): Boolean
} }
class ExposedKidioRepository : KidioRepository { class ExposedKidioRepository : KidioRepository {
@ -31,6 +32,27 @@ class ExposedKidioRepository : KidioRepository {
} }
} }
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
}
}
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? = DatabaseFactory.dbQuery { override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? = DatabaseFactory.dbQuery {
TasksTable.selectAll() TasksTable.selectAll()
.where { TasksTable.id eq taskId } .where { TasksTable.id eq taskId }
@ -52,12 +74,14 @@ class ExposedKidioRepository : KidioRepository {
} catch (e: Exception) { } catch (e: Exception) {
println("WARNUNG: Konnte nicht aus Redis lesen (Postgres-Fallback genutzt): ${e.message}") println("WARNUNG: Konnte nicht aus Redis lesen (Postgres-Fallback genutzt): ${e.message}")
} }
val dbSeconds = DatabaseFactory.dbQuery { val dbSeconds = DatabaseFactory.dbQuery {
ScreentimeTable.selectAll() ScreentimeTable.selectAll()
.where { ScreentimeTable.userId eq userId } .where { ScreentimeTable.userId eq userId }
.map { it[ScreentimeTable.remainingSeconds] } .map { it[ScreentimeTable.remainingSeconds] } // Korrigiert: Abfrage der Sekunden!
.singleOrNull() ?: 1800 .singleOrNull() ?: 1800
} }
try { try {
RedisFactory.getResource().use { jedis -> RedisFactory.getResource().use { jedis ->
jedis.setex(redisKey, 3600, dbSeconds.toString()) jedis.setex(redisKey, 3600, dbSeconds.toString())
@ -69,34 +93,36 @@ class ExposedKidioRepository : KidioRepository {
return dbSeconds 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 }) { val rowsUpdated = ScreentimeTable.update({ ScreentimeTable.userId eq userId }) {
it[remainingSeconds] = seconds it[remainingSeconds] = seconds
} }
if (rowsUpdated == 0) { if (rowsUpdated == 0) {
ScreentimeTable.insert { ScreentimeTable.insert {
it[ScreentimeTable.userId] = userId it[ScreentimeTable.userId] = userId
it[remainingSeconds] = seconds it[ScreentimeTable.remainingSeconds] = seconds // Korrigiert: Wert wird wieder mitgegeben!
it[parentPin] = "1234"
} }
} }
} }
override suspend fun findByUsername(username: String): User? = DatabaseFactory.dbQuery { override suspend fun findByEmail(email: String): User? = DatabaseFactory.dbQuery {
UserTable.selectAll() UserTable.selectAll()
.where { UserTable.username eq username } .where { UserTable.email eq email }
.map { .map {
User( User(
id = it[UserTable.id], id = it[UserTable.id],
username = it[UserTable.username], email = it[UserTable.email],
passwordHash = it[UserTable.passwordHash] passwordHash = it[UserTable.passwordHash],
familyName = it[UserTable.familyName],
childAge = it[UserTable.childAge],
parentPin = it[UserTable.parentPin]
) )
} }
.singleOrNull() .singleOrNull()
} }
override suspend fun verifyPassword(password: String, passwordHash: String): Boolean { override suspend fun verifyPassword(password: String, passwordHash: String): Boolean {
return password == passwordHash return BCrypt.checkpw(password, passwordHash)
} }
} }
@ -112,10 +138,7 @@ class InMemoryKidioRepository : KidioRepository {
private val screentimes = mutableMapOf( private val screentimes = mutableMapOf(
"default_user" to 1800 "default_user" to 1800
) )
private val users = mutableListOf<User>()
private val users = mutableListOf(
User(id = "1", username = "max", passwordHash = "geheim123")
)
override suspend fun getAllTasks(): List<Task> = tasks override suspend fun getAllTasks(): List<Task> = tasks
@ -127,11 +150,17 @@ class InMemoryKidioRepository : KidioRepository {
screentimes[userId] = seconds screentimes[userId] = seconds
} }
override suspend fun findByUsername(username: String): User? { override suspend fun findByEmail(email: String): User? {
return users.find { it.username == username } return users.find { it.email == email }
} }
override suspend fun verifyPassword(password: String, passwordHash: String): Boolean { override suspend fun verifyPassword(password: String, passwordHash: String): Boolean {
return password == passwordHash return BCrypt.checkpw(password, passwordHash)
}
override suspend fun registerUser(user: User): Boolean {
users.add(user)
screentimes[user.id] = 1800
return true
} }
} }

View file

@ -3,15 +3,53 @@ package com.oliver.kidio.backend.plugins
import com.oliver.kidio.backend.* import com.oliver.kidio.backend.*
import com.oliver.kidio.backend.domain.repository.KidioRepository import com.oliver.kidio.backend.domain.repository.KidioRepository
import com.oliver.kidio.backend.security.JwtService import com.oliver.kidio.backend.security.JwtService
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.authenticate 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.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import org.mindrot.jbcrypt.BCrypt
import java.util.UUID
fun Application.configureRouting(repository: KidioRepository, jwtService : JwtService) { fun Application.configureRouting(repository: KidioRepository, jwtService : JwtService) {
routing { routing {
post("/api/v1/auth/register") {
val registerRequest = call.receive<RegisterRequest>()
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 // Health-Check
get("/api/v1/health") { get("/api/v1/health") {
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!")) call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
@ -19,9 +57,27 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
post ("/api/v1/auth/login" ) { post ("/api/v1/auth/login" ) {
val loginRequest = call.receive<LoginRequest>() 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(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") { authenticate("auth-jwt") {
@ -35,9 +91,14 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
// 2. Antwort auf eine Aufgabe überprüfen // 2. Antwort auf eine Aufgabe überprüfen
post("/api/v1/tasks/verify") { post("/api/v1/tasks/verify") {
val request = call.receive<TaskVerifyRequest>() val request = call.receive<TaskVerifyRequest>()
val principal = call.principal<JWTPrincipal>()
val userId = principal?.payload?.getClaim("userId")?.asString()
if (userId == null){
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiges oder abgelaufenes Token"))
return@post
}
val taskInfo = repository.getTaskAnswerAndReward(request.taskId) val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
if (taskInfo == null) { if (taskInfo == null) {
call.respond( call.respond(
TaskVerifyResponse( TaskVerifyResponse(
@ -53,9 +114,9 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim() val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim()
if (isCorrect) { if (isCorrect) {
val currentScreentime = repository.getScreentime("default_user") val currentScreentime = repository.getScreentime(userId)
val newSeconds = currentScreentime + (reward * 60) val newSeconds = currentScreentime + (reward * 60)
repository.updateScreentime("default_user", newSeconds) repository.updateScreentime(userId, newSeconds)
call.respond( call.respond(
TaskVerifyResponse( TaskVerifyResponse(

View file

@ -8,7 +8,7 @@ import io.ktor.server.auth.jwt.*
/** /**
* Repräsentiert den authentifizierten Benutzer im Request-Kontext. * 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) { fun Application.configureSecurity(jwtService: JwtService) {
install(Authentication) { install(Authentication) {