Compare commits
1 commit
main
...
feat/backu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
453fefcec9 |
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,3 +11,4 @@ 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,2 +0,0 @@
|
||||||
#Wed Sep 02 15:03:29 CEST 2026
|
|
||||||
gradle.version=9.5.1
|
|
||||||
Binary file not shown.
Binary file not shown.
176
README.md
176
README.md
|
|
@ -1,171 +1,33 @@
|
||||||
# Kidio Backend
|
# Kidio_backend
|
||||||
|
|
||||||
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.
|
This project was created using the [Ktor Project Generator](https://start.ktor.io).
|
||||||
|
|
||||||
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.
|
Here are some useful links to get you started:
|
||||||
|
|
||||||
---
|
* [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
|
||||||
|
|
||||||
Hier ist eine Übersicht der implementierten Kernfunktionen des Kidio-Backends:
|
Here's a list of features included in this project:
|
||||||
|
|
||||||
| Name | Beschreibung |
|
| Name | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| **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
|
||||||
|
|
||||||
## Technologiestack
|
To build or run the project, use one of the following tasks:
|
||||||
|
|
||||||
- **Framework**: [Ktor](https://ktor.io/) (Kotlin-basiertes asynchrones Web-Framework)
|
| Task | Description |
|
||||||
- **Datenbank-ORM**: [JetBrains Exposed](https://github.com/JetBrains/Exposed)
|
|-------------------|-------------------|
|
||||||
- **Persistenz**: PostgreSQL
|
| `./gradlew test` | Run the tests |
|
||||||
- **Caching**: Redis (via Jedis Client)
|
| `./gradlew build` | Build the project |
|
||||||
- **Sicherheit & Auth**: JWT (Java JWT von auth0)
|
| `./gradlew run` | Run the server |
|
||||||
- **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 <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
|
|
||||||
```
|
```
|
||||||
|
2024-12-04 14:32:45.584 [main] INFO Application - Application started in 0.303 seconds.
|
||||||
Sobald der Server erfolgreich gestartet ist, ist er unter `http://localhost:8080` erreichbar.
|
2024-12-04 14:32:45.682 [main] INFO Application - Responding at http://0.0.0.0:8080
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ 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
BIN
src/.DS_Store
vendored
Binary file not shown.
BIN
src/main/.DS_Store
vendored
BIN
src/main/.DS_Store
vendored
Binary file not shown.
|
|
@ -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")
|
val options = text("options") // Kommagetrennte Liste der Antwortmöglichkeiten
|
||||||
val correctAnswer = varchar("correct_answer", 255)
|
val correctAnswer = varchar("correct_answer", 255)
|
||||||
val rewardMinutes = integer("reward_minutes")
|
val rewardMinutes = integer("reward_minutes")
|
||||||
|
|
||||||
|
|
@ -19,17 +19,15 @@ 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 email = varchar("email", 255).uniqueIndex()
|
val username = varchar("username", 50).index(isUnique = true)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
@ -37,12 +35,26 @@ object UserTable : Table("users") {
|
||||||
object DatabaseFactory {
|
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")
|
||||||
|
?: 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 host = System.getenv("DB_HOST") ?: config.propertyOrNull("storage.host")?.getString() ?: "localhost"
|
val dbUsername = System.getenv("DB_USER")
|
||||||
val port = System.getenv("DB_PORT") ?: config.propertyOrNull("storage.port")?.getString() ?: "5432"
|
?: config.propertyOrNull("storage.username")?.getString()
|
||||||
val dbName = System.getenv("DB_NAME") ?: config.propertyOrNull("storage.database")?.getString() ?: "kidio_db"
|
?: "postgres"
|
||||||
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 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"
|
||||||
|
|
||||||
|
|
@ -56,6 +68,7 @@ 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"
|
||||||
|
|
@ -72,22 +85,23 @@ object DatabaseFactory {
|
||||||
it[rewardMinutes] = 15
|
it[rewardMinutes] = 15
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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[email] = "test@kidio.de"
|
it[username] = "default_user"
|
||||||
it[passwordHash] = "1234" // Dummy-Hash für den Start
|
it[passwordHash] = "1234"
|
||||||
it[familyName] = "Familie Test"
|
|
||||||
it[childAge] = 8
|
|
||||||
it[parentPin] = "1234"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,4 +109,4 @@ object DatabaseFactory {
|
||||||
|
|
||||||
suspend fun <T> dbQuery(block: suspend () -> T): T =
|
suspend fun <T> dbQuery(block: suspend () -> T): T =
|
||||||
newSuspendedTransaction(Dispatchers.IO) { block() }
|
newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
val options: List<String>, // Antwortmöglichkeiten
|
||||||
val rewardMinutes: Int // Wie viel Belohnung es gibt
|
val rewardMinutes: Int // Wie viel Belohnung es gibt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -33,43 +33,20 @@ 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
|
|
||||||
)
|
|
||||||
|
|
@ -8,16 +8,15 @@ 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 {
|
||||||
|
|
@ -32,27 +31,6 @@ 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 }
|
||||||
|
|
@ -74,14 +52,12 @@ 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] } // Korrigiert: Abfrage der Sekunden!
|
.map { it[ScreentimeTable.remainingSeconds] }
|
||||||
.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())
|
||||||
|
|
@ -93,36 +69,34 @@ class ExposedKidioRepository : KidioRepository {
|
||||||
return dbSeconds
|
return dbSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateScreentime(userId: String, seconds: Int) = DatabaseFactory.dbQuery {
|
override suspend fun updateScreentime(userId: String, seconds: Int): Unit = 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[ScreentimeTable.remainingSeconds] = seconds // Korrigiert: Wert wird wieder mitgegeben!
|
it[remainingSeconds] = seconds
|
||||||
|
it[parentPin] = "1234"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByEmail(email: String): User? = DatabaseFactory.dbQuery {
|
override suspend fun findByUsername(username: String): User? = DatabaseFactory.dbQuery {
|
||||||
UserTable.selectAll()
|
UserTable.selectAll()
|
||||||
.where { UserTable.email eq email }
|
.where { UserTable.username eq username }
|
||||||
.map {
|
.map {
|
||||||
User(
|
User(
|
||||||
id = it[UserTable.id],
|
id = it[UserTable.id],
|
||||||
email = it[UserTable.email],
|
username = it[UserTable.username],
|
||||||
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 BCrypt.checkpw(password, passwordHash)
|
return password == passwordHash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +112,10 @@ 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
|
||||||
|
|
||||||
|
|
@ -150,17 +127,11 @@ class InMemoryKidioRepository : KidioRepository {
|
||||||
screentimes[userId] = seconds
|
screentimes[userId] = seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByEmail(email: String): User? {
|
override suspend fun findByUsername(username: String): User? {
|
||||||
return users.find { it.email == email }
|
return users.find { it.username == username }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun verifyPassword(password: String, passwordHash: String): Boolean {
|
override suspend fun verifyPassword(password: String, passwordHash: String): Boolean {
|
||||||
return BCrypt.checkpw(password, passwordHash)
|
return password == passwordHash
|
||||||
}
|
}
|
||||||
|
}
|
||||||
override suspend fun registerUser(user: User): Boolean {
|
|
||||||
users.add(user)
|
|
||||||
screentimes[user.id] = 1800
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,53 +3,15 @@ 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!"))
|
||||||
|
|
@ -57,27 +19,9 @@ 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") {
|
||||||
|
|
@ -89,53 +33,48 @@ 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){
|
val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
|
||||||
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiges oder abgelaufenes Token"))
|
|
||||||
|
if (taskInfo == null) {
|
||||||
|
call.respond(
|
||||||
|
TaskVerifyResponse(
|
||||||
|
isCorrect = false,
|
||||||
|
earnedMinutes = 0,
|
||||||
|
message = "Aufgabe nicht gefunden!"
|
||||||
|
)
|
||||||
|
)
|
||||||
return@post
|
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
|
|
||||||
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!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
call.respond(
|
||||||
|
TaskVerifyResponse(
|
||||||
|
isCorrect = false,
|
||||||
|
earnedMinutes = 0,
|
||||||
|
message = "Schade, das war leider falsch. Versuche es nochmal!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Bildschirmzeit synchronisieren
|
// 3. Bildschirmzeit synchronisieren
|
||||||
post("/api/v1/screentime/sync") {
|
post("/api/v1/screentime/sync") {
|
||||||
val request = call.receive<SyncRequest>()
|
val request = call.receive<SyncRequest>()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
data class UserPrincipal(val userId: String) : Principal
|
||||||
|
|
||||||
fun Application.configureSecurity(jwtService: JwtService) {
|
fun Application.configureSecurity(jwtService: JwtService) {
|
||||||
install(Authentication) {
|
install(Authentication) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue