new task fetch integration check
Some checks are pending
CI Pipeline / lint-and-test (push) Waiting to run
Some checks are pending
CI Pipeline / lint-and-test (push) Waiting to run
This commit is contained in:
parent
a57870f880
commit
ba3e683297
|
|
@ -0,0 +1,10 @@
|
|||
package com.oliver.kidio.backend.data.tables
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
object ScreentimeTable : Table("user_screentime") {
|
||||
val userId = varchar("user_id", 50)
|
||||
val remainingSeconds = integer("remaining_seconds")
|
||||
|
||||
override val primaryKey = PrimaryKey(userId)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.oliver.kidio.backend.data.tables
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
/*
|
||||
* ERSETZT die bisherige TasksTable.kt komplett.
|
||||
*
|
||||
* Weg: options (Komma-String), correctAnswer (ein String)
|
||||
* Hin: prompt_json, content_json, solution_json (je ein JSON-Text, siehe TaskModels.kt)
|
||||
*
|
||||
* WICHTIG - Migration: Diese Änderung ist nicht abwärtskompatibel zu vorhandenen Zeilen,
|
||||
* die noch die alten Spalten "options"/"correct_answer" nutzen. Zwei Optionen:
|
||||
*
|
||||
* 1. (Empfohlen für Dev-Datenbanken ohne echte Nutzerdaten) Tabelle leeren und per
|
||||
* ./gradlew runIngestion neu befüllen lassen, siehe H5PIngestionCli.kt.
|
||||
*
|
||||
* 2. (Falls du bestehende Zeilen behalten willst) Vor dem Deploy ein einmaliges
|
||||
* Migrationsskript schreiben, das pro Zeile aus options+correctAnswer ein
|
||||
* MultipleChoiceContent/ChoiceSolution baut und in die neuen Spalten schreibt.
|
||||
* Sag Bescheid, dann schreibe ich dir das als eigenen Schritt.
|
||||
*/
|
||||
object TasksTable : Table("tasks") {
|
||||
val id = varchar("id", 64)
|
||||
val externalSource = varchar("external_source", 32).default("local")
|
||||
val sourceUrl = text("source_url").nullable()
|
||||
val license = varchar("license", 100).default("CC BY-SA 4.0")
|
||||
val subject = varchar("subject", 64).default("Mathematik")
|
||||
val minAge = integer("min_age").default(6)
|
||||
val maxAge = integer("max_age").default(12)
|
||||
val taskType = varchar("task_type", 32).default("MULTIPLE_CHOICE")
|
||||
|
||||
// NEU: ersetzen "question", "options", "correctAnswer"
|
||||
val promptJson = text("prompt_json")
|
||||
val contentJson = text("content_json")
|
||||
val solutionJson = text("solution_json")
|
||||
|
||||
val rewardMinutes = integer("reward_minutes").default(5)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index("idx_tasks_filter", false, subject, minAge, maxAge)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.oliver.kidio.backend.data.tables
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
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).default("Familie")
|
||||
val childAge = integer("child_age").default(8)
|
||||
val parentPin = varchar("parent_pin", 4).default("1234")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
|
|
@ -1,21 +1,162 @@
|
|||
package com.oliver.kidio.backend
|
||||
package com.oliver.kidio.backend.domain.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
// Eine Lernaufgabe (z. B. Mathe)
|
||||
/*
|
||||
* ERSETZT die bisherige TaskModels.kt komplett.
|
||||
*
|
||||
* Warum: Task/ImportedTask hatten "options: List<String>" + "correctAnswer: String".
|
||||
* Das kann nur "eine richtige Antwort aus einer Liste" abbilden (Multiple Choice,
|
||||
* Wahr/Falsch). Lückentexte, Reihenfolgen oder Paare passen strukturell nicht rein.
|
||||
*
|
||||
* Neues Prinzip: Jede Aufgabe hat einen TYP (sealed interface TaskContent).
|
||||
* Die Lösung (TaskSolution) ist ein eigenes, getrenntes Objekt und geht NIE an den Client.
|
||||
*
|
||||
* ACHTUNG (Kompilierfehler erwartet): Nach diesem Schritt melden ExposedKidioRepository.kt,
|
||||
* TasksTable.kt und TaskRoutes.kt Fehler, weil sie noch die alten Klassen Task/ImportedTask/
|
||||
* TaskVerifyRequest referenzieren. Das ist normal - die fixen wir in Schritt 2 bis 4.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------- Bausteine
|
||||
|
||||
@Serializable
|
||||
data class Media(
|
||||
val emoji: String? = null,
|
||||
val imageUrl: String? = null,
|
||||
val audioUrl: String? = null,
|
||||
val alt: String? = null,
|
||||
// repeat = 3 mit emoji "🍎" zeigt drei Äpfel, praktisch zum Zählen für Kleinkinder
|
||||
val repeat: Int = 1,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Choice(
|
||||
val id: String,
|
||||
val label: String? = null,
|
||||
val media: Media? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Prompt(
|
||||
val text: String,
|
||||
val media: Media? = null,
|
||||
// Die App liest den Text per Text-to-Speech vor (wichtig für Kinder, die noch nicht lesen)
|
||||
val speakAloud: Boolean = false,
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------- Inhalt (geht an die App)
|
||||
|
||||
@Serializable
|
||||
sealed interface TaskContent
|
||||
|
||||
/** Ein oder mehrere richtige Antworten. Auch für Bild-Antworten (Kleinkinder). */
|
||||
@Serializable
|
||||
@SerialName("MULTIPLE_CHOICE")
|
||||
data class MultipleChoiceContent(
|
||||
val choices: List<Choice>,
|
||||
val multiSelect: Boolean = false,
|
||||
) : TaskContent
|
||||
|
||||
/** Zwei große Knöpfe. Antwort-IDs sind immer "true" und "false". */
|
||||
@Serializable
|
||||
@SerialName("TRUE_FALSE")
|
||||
object TrueFalseContent : TaskContent {
|
||||
const val TRUE_ID = "true"
|
||||
const val FALSE_ID = "false"
|
||||
}
|
||||
|
||||
/** Lückentext. Im Template stehen {1}, {2}, ... für die Lücken. */
|
||||
@Serializable
|
||||
@SerialName("FILL_BLANKS")
|
||||
data class FillBlanksContent(
|
||||
val template: String,
|
||||
val blanks: Int,
|
||||
val wordBank: List<String> = emptyList(),
|
||||
) : TaskContent
|
||||
|
||||
/** Elemente in die richtige Reihenfolge bringen. Kommt gemischt an. */
|
||||
@Serializable
|
||||
@SerialName("SORT")
|
||||
data class SortContent(
|
||||
val items: List<Choice>,
|
||||
) : TaskContent
|
||||
|
||||
/** Paare verbinden (links zu rechts). */
|
||||
@Serializable
|
||||
@SerialName("MATCH_PAIRS")
|
||||
data class MatchPairsContent(
|
||||
val left: List<Choice>,
|
||||
val right: List<Choice>,
|
||||
) : TaskContent
|
||||
|
||||
/** Die richtigen Wörter im Text markieren. */
|
||||
@Serializable
|
||||
@SerialName("MARK_WORDS")
|
||||
data class MarkWordsContent(
|
||||
val tokens: List<Choice>,
|
||||
) : TaskContent
|
||||
|
||||
// ---------------------------------------------------------------- Lösung (bleibt auf dem Server)
|
||||
|
||||
@Serializable
|
||||
sealed interface TaskSolution
|
||||
|
||||
@Serializable
|
||||
@SerialName("CHOICE")
|
||||
data class ChoiceSolution(val correctIds: Set<String>) : TaskSolution
|
||||
|
||||
/** Pro Lücke eine Liste akzeptierter Schreibweisen. */
|
||||
@Serializable
|
||||
@SerialName("FILL")
|
||||
data class FillSolution(val accepted: List<List<String>>) : TaskSolution
|
||||
|
||||
@Serializable
|
||||
@SerialName("ORDER")
|
||||
data class OrderSolution(val orderedIds: List<String>) : TaskSolution
|
||||
|
||||
@Serializable
|
||||
@SerialName("PAIRS")
|
||||
data class PairsSolution(val pairs: Map<String, String>) : TaskSolution
|
||||
|
||||
// ---------------------------------------------------------------- Antwort des Kindes
|
||||
|
||||
@Serializable
|
||||
sealed interface TaskAnswer
|
||||
|
||||
@Serializable
|
||||
@SerialName("CHOICE")
|
||||
data class ChoiceAnswer(val selectedIds: Set<String>) : TaskAnswer
|
||||
|
||||
@Serializable
|
||||
@SerialName("FILL")
|
||||
data class FillAnswer(val values: List<String>) : TaskAnswer
|
||||
|
||||
@Serializable
|
||||
@SerialName("ORDER")
|
||||
data class OrderAnswer(val orderedIds: List<String>) : TaskAnswer
|
||||
|
||||
@Serializable
|
||||
@SerialName("PAIRS")
|
||||
data class PairsAnswer(val pairs: Map<String, String>) : TaskAnswer
|
||||
|
||||
// ---------------------------------------------------------------- API-Objekte
|
||||
|
||||
/** Ersetzt die alte "Task"-Klasse. Geht an die App, ohne Lösung. */
|
||||
@Serializable
|
||||
data class Task(
|
||||
val id: String,
|
||||
val subject: String = "Mathematik",
|
||||
val minAge: Int = 6,
|
||||
val maxAge: Int = 12,
|
||||
val question: String,
|
||||
val options: List<String>,
|
||||
val prompt: Prompt,
|
||||
val content: TaskContent,
|
||||
val rewardMinutes: Int,
|
||||
val license: String? = null,
|
||||
val sourceUrl: String? = null,
|
||||
)
|
||||
|
||||
// Interne Datenklasse für importierte Aufgaben (Ingestion)
|
||||
/** Ersetzt die alte "ImportedTask"-Klasse. Interne Datenklasse für den Import. */
|
||||
data class ImportedTask(
|
||||
val id: String,
|
||||
val externalSource: String = "zum.de",
|
||||
|
|
@ -24,21 +165,20 @@ data class ImportedTask(
|
|||
val subject: String,
|
||||
val minAge: Int,
|
||||
val maxAge: Int,
|
||||
val taskType: String = "MULTIPLE_CHOICE",
|
||||
val question: String,
|
||||
val options: List<String>,
|
||||
val correctAnswer: String,
|
||||
val prompt: Prompt,
|
||||
val content: TaskContent,
|
||||
val solution: TaskSolution,
|
||||
val rewardMinutes: Int = 5,
|
||||
)
|
||||
|
||||
// Anfrage des Kindes, wenn es eine Aufgabe löst
|
||||
/** Ersetzt die alte "TaskVerifyRequest": statt eines Strings jetzt eine typisierte Antwort. */
|
||||
@Serializable
|
||||
data class TaskVerifyRequest(
|
||||
val taskId: String,
|
||||
val selectedAnswer: String,
|
||||
val answer: TaskAnswer,
|
||||
)
|
||||
|
||||
// Antwort vom Server nach der Aufgaben-Prüfung
|
||||
/** Unverändert gegenüber vorher. */
|
||||
@Serializable
|
||||
data class TaskVerifyResponse(
|
||||
val isCorrect: Boolean,
|
||||
|
|
@ -46,50 +186,32 @@ data class TaskVerifyResponse(
|
|||
val message: String,
|
||||
)
|
||||
|
||||
// Datenabgleich für die Bildschirmzeit (Sync)
|
||||
@Serializable
|
||||
data class SyncRequest(
|
||||
val userId: String,
|
||||
val usedSecondsSinceLastSync: Int,
|
||||
)
|
||||
// ---------------------------------------------------------------- Prüfung
|
||||
|
||||
@Serializable
|
||||
data class SyncResponse(
|
||||
val remainingSeconds: Int,
|
||||
val isBlocked: Boolean,
|
||||
)
|
||||
object TaskChecker {
|
||||
fun isCorrect(
|
||||
solution: TaskSolution,
|
||||
answer: TaskAnswer,
|
||||
): Boolean =
|
||||
when {
|
||||
solution is ChoiceSolution && answer is ChoiceAnswer ->
|
||||
solution.correctIds == answer.selectedIds
|
||||
|
||||
// --- NEU: AUTHENTIFIZIERUNG ---
|
||||
solution is FillSolution && answer is FillAnswer ->
|
||||
solution.accepted.size == answer.values.size &&
|
||||
solution.accepted.indices.all { i ->
|
||||
solution.accepted[i].any { normalize(it) == normalize(answer.values[i]) }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RegisterRequest(
|
||||
val email: String,
|
||||
val passwordPlain: String,
|
||||
val familyName: String,
|
||||
val childAge: Int,
|
||||
val parentPin: String,
|
||||
)
|
||||
solution is OrderSolution && answer is OrderAnswer ->
|
||||
solution.orderedIds == answer.orderedIds
|
||||
|
||||
@Serializable
|
||||
data class LoginRequest(
|
||||
val email: String,
|
||||
val passwordPlain: String,
|
||||
)
|
||||
solution is PairsSolution && answer is PairsAnswer ->
|
||||
solution.pairs == answer.pairs
|
||||
|
||||
@Serializable
|
||||
data class AuthResponse(
|
||||
val token: String,
|
||||
val userId: String,
|
||||
val familyName: String,
|
||||
val childAge: Int,
|
||||
)
|
||||
// Typ von Antwort und Lösung passen nicht zusammen: immer falsch, nie einen Fehler werfen
|
||||
else -> false
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
private fun normalize(s: String): String = s.trim().lowercase().replace(Regex("\\s+"), " ")
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.oliver.kidio.backend.domain.models
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/*
|
||||
* NEUE DATEI (noch nicht vorhanden in deinem Projekt).
|
||||
* Ablage: src/main/kotlin/com/oliver/kidio/backend/domain/models/TaskJson.kt
|
||||
*
|
||||
* Eine gemeinsame Json-Konfiguration, die überall dort verwendet wird, wo TaskContent,
|
||||
* TaskSolution oder TaskAnswer serialisiert/deserialisiert werden: beim Schreiben/Lesen
|
||||
* der DB-Spalten (content_json, solution_json, prompt_json) und im H5P-Mapper.
|
||||
*
|
||||
* classDiscriminator = "type" sorgt dafür, dass im JSON ein Feld "type" steht
|
||||
* (z. B. "type": "MULTIPLE_CHOICE"), damit die App weiß, welche Variante sie bekommt.
|
||||
*/
|
||||
object TaskJson {
|
||||
val json: Json =
|
||||
Json {
|
||||
classDiscriminator = "type"
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
package com.oliver.kidio.backend.domain.repository
|
||||
|
||||
import com.oliver.kidio.backend.data.database.DatabaseFactory
|
||||
import com.oliver.kidio.backend.data.database.RedisFactory
|
||||
import com.oliver.kidio.backend.data.tables.ScreentimeTable
|
||||
import com.oliver.kidio.backend.data.tables.TasksTable
|
||||
import com.oliver.kidio.backend.data.tables.UserTable
|
||||
import com.oliver.kidio.backend.domain.models.FillBlanksContent
|
||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||
import com.oliver.kidio.backend.domain.models.MarkWordsContent
|
||||
import com.oliver.kidio.backend.domain.models.MatchPairsContent
|
||||
import com.oliver.kidio.backend.domain.models.MultipleChoiceContent
|
||||
import com.oliver.kidio.backend.domain.models.Prompt
|
||||
import com.oliver.kidio.backend.domain.models.SortContent
|
||||
import com.oliver.kidio.backend.domain.models.Task
|
||||
import com.oliver.kidio.backend.domain.models.TaskContent
|
||||
import com.oliver.kidio.backend.domain.models.TaskJson
|
||||
import com.oliver.kidio.backend.domain.models.TaskSolution
|
||||
import com.oliver.kidio.backend.domain.models.TrueFalseContent
|
||||
import com.oliver.kidio.backend.domain.models.User
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
import kotlin.collections.singleOrNull
|
||||
|
||||
/*
|
||||
* ERSETZT die bisherige ExposedKidioRepository.kt.
|
||||
* Geändert: getTasks, upsertTask, getTaskAnswerAndReward (JSON-Spalten statt options/correctAnswer).
|
||||
* Unverändert: getUserById, registerUser, getScreentime, updateScreentime, findByEmail,
|
||||
* findById, verifyPassword, getExistingTaskIds.
|
||||
*/
|
||||
class ExposedKidioRepository : KidioRepository {
|
||||
override suspend fun getTasks(
|
||||
subject: String?,
|
||||
age: Int?,
|
||||
limit: Int,
|
||||
): List<Task> =
|
||||
DatabaseFactory.dbQuery {
|
||||
var query = TasksTable.selectAll()
|
||||
if (!subject.isNullOrBlank()) {
|
||||
query = query.where { TasksTable.subject.lowerCase() eq subject.trim().lowercase() }
|
||||
}
|
||||
if (age != null) {
|
||||
query = query.andWhere { (TasksTable.minAge lessEq age) and (TasksTable.maxAge greaterEq age) }
|
||||
}
|
||||
val safeLimit = if (limit <= 0) 10 else limit
|
||||
query.limit(safeLimit).mapNotNull { row -> rowToTaskOrNull(row) }
|
||||
}
|
||||
|
||||
/** Baut eine Task aus einer DB-Zeile. Bei kaputtem JSON wird die Zeile übersprungen statt den Server abstürzen zu lassen. */
|
||||
private fun rowToTaskOrNull(row: ResultRow): Task? =
|
||||
try {
|
||||
Task(
|
||||
id = row[TasksTable.id],
|
||||
subject = row[TasksTable.subject],
|
||||
minAge = row[TasksTable.minAge],
|
||||
maxAge = row[TasksTable.maxAge],
|
||||
prompt = TaskJson.json.decodeFromString<Prompt>(row[TasksTable.promptJson]),
|
||||
content = TaskJson.json.decodeFromString<TaskContent>(row[TasksTable.contentJson]),
|
||||
rewardMinutes = row[TasksTable.rewardMinutes],
|
||||
license = row[TasksTable.license],
|
||||
sourceUrl = row[TasksTable.sourceUrl],
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Task ${row[TasksTable.id]} konnte nicht gelesen werden (kaputtes JSON?): ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
override suspend fun getUserById(userId: String): User? =
|
||||
DatabaseFactory.dbQuery {
|
||||
UserTable.selectAll()
|
||||
.where { UserTable.id eq userId }
|
||||
.map {
|
||||
User(
|
||||
id = it[UserTable.id],
|
||||
email = it[UserTable.email],
|
||||
passwordHash = it[UserTable.passwordHash],
|
||||
familyName = it[UserTable.familyName],
|
||||
childAge = it[UserTable.childAge],
|
||||
parentPin = it[UserTable.parentPin],
|
||||
)
|
||||
}
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||
|
||||
override suspend fun getExistingTaskIds(externalSource: String): Set<String> =
|
||||
DatabaseFactory.dbQuery {
|
||||
TasksTable
|
||||
.select(TasksTable.id)
|
||||
.where { TasksTable.externalSource eq externalSource }
|
||||
.map { it[TasksTable.id] }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override suspend fun upsertTask(task: ImportedTask): Boolean =
|
||||
DatabaseFactory.dbQuery {
|
||||
try {
|
||||
val promptJson = TaskJson.json.encodeToString(task.prompt)
|
||||
val contentJson = TaskJson.json.encodeToString(task.content)
|
||||
val solutionJson = TaskJson.json.encodeToString(task.solution)
|
||||
|
||||
val updated =
|
||||
TasksTable.update({ TasksTable.id eq task.id }) {
|
||||
it[externalSource] = task.externalSource
|
||||
it[sourceUrl] = task.sourceUrl
|
||||
it[license] = task.license
|
||||
it[subject] = task.subject
|
||||
it[minAge] = task.minAge
|
||||
it[maxAge] = task.maxAge
|
||||
it[taskType] = task.content.typeName()
|
||||
it[TasksTable.promptJson] = promptJson
|
||||
it[TasksTable.contentJson] = contentJson
|
||||
it[TasksTable.solutionJson] = solutionJson
|
||||
it[rewardMinutes] = task.rewardMinutes
|
||||
}
|
||||
if (updated == 0) {
|
||||
TasksTable.insert {
|
||||
it[id] = task.id
|
||||
it[externalSource] = task.externalSource
|
||||
it[sourceUrl] = task.sourceUrl
|
||||
it[license] = task.license
|
||||
it[subject] = task.subject
|
||||
it[minAge] = task.minAge
|
||||
it[maxAge] = task.maxAge
|
||||
it[taskType] = task.content.typeName()
|
||||
it[TasksTable.promptJson] = promptJson
|
||||
it[TasksTable.contentJson] = contentJson
|
||||
it[TasksTable.solutionJson] = solutionJson
|
||||
it[rewardMinutes] = task.rewardMinutes
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
println("FEHLER beim Upsert von Task ${task.id}: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
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<TaskSolution, Int>? =
|
||||
DatabaseFactory.dbQuery {
|
||||
TasksTable.selectAll()
|
||||
.where { TasksTable.id eq taskId }
|
||||
.map {
|
||||
val solution = TaskJson.json.decodeFromString<TaskSolution>(it[TasksTable.solutionJson])
|
||||
solution to it[TasksTable.rewardMinutes]
|
||||
}
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getScreentime(userId: String): Int {
|
||||
val redisKey = "screentime:$userId"
|
||||
|
||||
try {
|
||||
RedisFactory.getResource().use { jedis ->
|
||||
val cachedSeconds = jedis.get(redisKey)
|
||||
if (cachedSeconds != null) {
|
||||
println("DEBUG: Bildschirmzeit für $userId aus REDIS geladen: $cachedSeconds Sek")
|
||||
return cachedSeconds.toInt()
|
||||
}
|
||||
}
|
||||
} 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())
|
||||
println("DEBUG: Bildschirmzeit für $userId in REDIS gecacht.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Konnte Wert nicht in Redis speichern: ${e.message}")
|
||||
}
|
||||
return dbSeconds
|
||||
}
|
||||
|
||||
override suspend fun updateScreentime(
|
||||
userId: String,
|
||||
seconds: Int,
|
||||
) {
|
||||
DatabaseFactory.dbQuery {
|
||||
val rowsUpdated =
|
||||
ScreentimeTable.update({ ScreentimeTable.userId eq userId }) {
|
||||
it[remainingSeconds] = seconds
|
||||
}
|
||||
if (rowsUpdated == 0) {
|
||||
ScreentimeTable.insert {
|
||||
it[ScreentimeTable.userId] = userId
|
||||
it[ScreentimeTable.remainingSeconds] = seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
val redisKey = "screentime:$userId"
|
||||
try {
|
||||
RedisFactory.getResource().use { jedis ->
|
||||
jedis.setex(redisKey, 3600, seconds.toString())
|
||||
println("DEBUG: Bildschirmzeit für $userId in REDIS aktualisiert auf $seconds Sek")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Konnte Redis nicht aktualisieren: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByEmail(email: String): User? =
|
||||
DatabaseFactory.dbQuery {
|
||||
UserTable.selectAll()
|
||||
.where { UserTable.email eq email }
|
||||
.map {
|
||||
User(
|
||||
id = it[UserTable.id],
|
||||
email = it[UserTable.email],
|
||||
passwordHash = it[UserTable.passwordHash],
|
||||
familyName = it[UserTable.familyName],
|
||||
childAge = it[UserTable.childAge],
|
||||
parentPin = it[UserTable.parentPin],
|
||||
)
|
||||
}
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findById(userId: String): User? =
|
||||
DatabaseFactory.dbQuery {
|
||||
UserTable.selectAll()
|
||||
.where { UserTable.id eq userId }
|
||||
.map {
|
||||
User(
|
||||
id = it[UserTable.id],
|
||||
email = it[UserTable.email],
|
||||
passwordHash = it[UserTable.passwordHash],
|
||||
familyName = it[UserTable.familyName],
|
||||
childAge = it[UserTable.childAge],
|
||||
parentPin = it[UserTable.parentPin],
|
||||
)
|
||||
}
|
||||
.singian leOrNull()
|
||||
}
|
||||
|
||||
override suspend fun verifyPassword(
|
||||
password: String,
|
||||
passwordHash: String,
|
||||
): Boolean {
|
||||
return BCrypt.checkpw(password, passwordHash)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Aufgabentyp als String für die taskType-Spalte (nur zum Filtern/Debuggen
|
||||
* in der DB, nicht Teil der eigentlichen Prüf-Logik - die läuft über TaskChecker).
|
||||
* Bewusst als explizites "when" statt Reflection, damit der Compiler bei einer neuen
|
||||
* TaskContent-Variante hier einen Fehler wirft ("not exhaustive"), falls sie vergessen wird.
|
||||
*/
|
||||
private fun TaskContent.typeName(): String =
|
||||
when (this) {
|
||||
is MultipleChoiceContent -> "MULTIPLE_CHOICE"
|
||||
is TrueFalseContent -> "TRUE_FALSE"
|
||||
is FillBlanksContent -> "FILL_BLANKS"
|
||||
is SortContent -> "SORT"
|
||||
is MatchPairsContent -> "MATCH_PAIRS"
|
||||
is MarkWordsContent -> "MARK_WORDS"
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.oliver.kidio.backend.domain.repository
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||
import com.oliver.kidio.backend.domain.models.Task
|
||||
import com.oliver.kidio.backend.domain.models.TaskSolution
|
||||
import com.oliver.kidio.backend.domain.models.User
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
|
||||
/*
|
||||
* ERSETZT die bisherige InMemoryKidioRepository.kt.
|
||||
* Wird u.a. in Tests genutzt (ServerTest o.ä.) - Signatur muss zum neuen KidioRepository-
|
||||
* Interface passen, sonst kompiliert das Test-Modul nicht mehr.
|
||||
*/
|
||||
class InMemoryKidioRepository : KidioRepository {
|
||||
private val tasks = mutableListOf<ImportedTask>()
|
||||
|
||||
private val screentimes =
|
||||
mutableMapOf(
|
||||
"default_user" to 1800,
|
||||
)
|
||||
private val users = mutableListOf<User>()
|
||||
|
||||
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||
|
||||
override suspend fun getTasks(
|
||||
subject: String?,
|
||||
age: Int?,
|
||||
limit: Int,
|
||||
): List<Task> {
|
||||
val safeLimit = if (limit <= 0) 10 else limit
|
||||
return tasks.filter { task ->
|
||||
val matchSubject = subject.isNullOrBlank() || task.subject.equals(subject.trim(), ignoreCase = true)
|
||||
val matchAge = age == null || (task.minAge <= age && task.maxAge >= age)
|
||||
matchSubject && matchAge
|
||||
}.take(safeLimit).map {
|
||||
Task(
|
||||
id = it.id,
|
||||
subject = it.subject,
|
||||
minAge = it.minAge,
|
||||
maxAge = it.maxAge,
|
||||
prompt = it.prompt,
|
||||
content = it.content,
|
||||
rewardMinutes = it.rewardMinutes,
|
||||
license = it.license,
|
||||
sourceUrl = it.sourceUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserById(userId: String): User? {
|
||||
return users.find { it.id == userId }
|
||||
}
|
||||
|
||||
override suspend fun getExistingTaskIds(externalSource: String): Set<String> {
|
||||
return tasks
|
||||
.filter { it.externalSource == externalSource }
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override suspend fun upsertTask(task: ImportedTask): Boolean {
|
||||
tasks.removeIf { it.id == task.id }
|
||||
tasks.add(task)
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<TaskSolution, Int>? {
|
||||
val task = tasks.find { it.id == taskId } ?: return null
|
||||
return task.solution to task.rewardMinutes
|
||||
}
|
||||
|
||||
override suspend fun getScreentime(userId: String): Int = screentimes[userId] ?: 1800
|
||||
|
||||
override suspend fun updateScreentime(
|
||||
userId: String,
|
||||
seconds: Int,
|
||||
) {
|
||||
screentimes[userId] = seconds
|
||||
}
|
||||
|
||||
override suspend fun findByEmail(email: String): User? {
|
||||
return users.find { it.email == email }
|
||||
}
|
||||
|
||||
override suspend fun findById(userId: String): User? {
|
||||
return users.find { it.id == userId }
|
||||
}
|
||||
|
||||
override suspend fun verifyPassword(
|
||||
password: String,
|
||||
passwordHash: String,
|
||||
): Boolean {
|
||||
return BCrypt.checkpw(password, passwordHash)
|
||||
}
|
||||
|
||||
override suspend fun registerUser(user: User): Boolean {
|
||||
users.add(user)
|
||||
screentimes[user.id] = 1800
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,379 +1,21 @@
|
|||
package com.oliver.kidio.backend.domain.repository
|
||||
|
||||
import com.oliver.kidio.backend.ImportedTask
|
||||
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.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.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
|
||||
import org.jetbrains.exposed.sql.lowerCase
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||
import com.oliver.kidio.backend.domain.models.Task
|
||||
import com.oliver.kidio.backend.domain.models.TaskSolution
|
||||
import com.oliver.kidio.backend.domain.models.User
|
||||
|
||||
interface KidioRepository {
|
||||
suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||
|
||||
suspend fun getUserById(userId: String): User?
|
||||
|
||||
suspend fun getTasks(
|
||||
subject: String? = null,
|
||||
age: Int? = null,
|
||||
limit: Int = 10,
|
||||
): List<Task>
|
||||
|
||||
// Mit suspend markiert, damit es zur DatabaseFactory.dbQuery Architektur passt
|
||||
suspend fun findById(userId: String): User?
|
||||
suspend fun getTasks(subject: String? = null, age: Int? = null, limit: Int = 10): List<Task>
|
||||
suspend fun getExistingTaskIds(externalSource: String = "zum.de"): Set<String>
|
||||
|
||||
suspend fun upsertTask(task: ImportedTask): Boolean
|
||||
|
||||
suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>?
|
||||
|
||||
suspend fun getTaskAnswerAndReward(taskId: String): Pair<TaskSolution, 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 verifyPassword(
|
||||
password: String,
|
||||
passwordHash: String,
|
||||
): Boolean
|
||||
|
||||
suspend fun verifyPassword(password: String, passwordHash: String): Boolean
|
||||
suspend fun registerUser(user: User): Boolean
|
||||
}
|
||||
|
||||
class ExposedKidioRepository : KidioRepository {
|
||||
override suspend fun getTasks(
|
||||
subject: String?,
|
||||
age: Int?,
|
||||
limit: Int,
|
||||
): List<Task> =
|
||||
DatabaseFactory.dbQuery {
|
||||
var query = TasksTable.selectAll()
|
||||
if (!subject.isNullOrBlank()) {
|
||||
query = query.where { TasksTable.subject.lowerCase() eq subject.trim().lowercase() }
|
||||
}
|
||||
if (age != null) {
|
||||
query = query.andWhere { (TasksTable.minAge lessEq age) and (TasksTable.maxAge greaterEq age) }
|
||||
}
|
||||
val safeLimit = if (limit <= 0) 10 else limit
|
||||
query.limit(safeLimit).map {
|
||||
Task(
|
||||
id = it[TasksTable.id],
|
||||
subject = it[TasksTable.subject],
|
||||
minAge = it[TasksTable.minAge],
|
||||
maxAge = it[TasksTable.maxAge],
|
||||
question = it[TasksTable.question],
|
||||
options = it[TasksTable.options].split(",").map { opt -> opt.trim() },
|
||||
rewardMinutes = it[TasksTable.rewardMinutes],
|
||||
license = it[TasksTable.license],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserById(userId: String): User? =
|
||||
DatabaseFactory.dbQuery {
|
||||
UserTable.selectAll()
|
||||
.where { UserTable.id eq userId }
|
||||
.map {
|
||||
User(
|
||||
id = it[UserTable.id],
|
||||
email = it[UserTable.email],
|
||||
passwordHash = it[UserTable.passwordHash],
|
||||
familyName = it[UserTable.familyName],
|
||||
childAge = it[UserTable.childAge],
|
||||
parentPin = it[UserTable.parentPin],
|
||||
)
|
||||
}
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||
|
||||
// NEU: Implementierung für Exposed (PostgreSQL)
|
||||
override suspend fun getExistingTaskIds(externalSource: String): Set<String> =
|
||||
DatabaseFactory.dbQuery {
|
||||
TasksTable
|
||||
.select(TasksTable.id) // Wir brauchen nur die ID-Spalte
|
||||
.where { TasksTable.externalSource eq externalSource }
|
||||
.map { it[TasksTable.id] }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override suspend fun upsertTask(task: ImportedTask): Boolean =
|
||||
DatabaseFactory.dbQuery {
|
||||
try {
|
||||
val updated =
|
||||
TasksTable.update({ TasksTable.id eq task.id }) {
|
||||
it[externalSource] = task.externalSource
|
||||
it[sourceUrl] = task.sourceUrl
|
||||
it[license] = task.license
|
||||
it[subject] = task.subject
|
||||
it[minAge] = task.minAge
|
||||
it[maxAge] = task.maxAge
|
||||
it[taskType] = task.taskType
|
||||
it[question] = task.question
|
||||
it[options] = task.options.joinToString(",")
|
||||
it[correctAnswer] = task.correctAnswer
|
||||
it[rewardMinutes] = task.rewardMinutes
|
||||
}
|
||||
if (updated == 0) {
|
||||
TasksTable.insert {
|
||||
it[id] = task.id
|
||||
it[externalSource] = task.externalSource
|
||||
it[sourceUrl] = task.sourceUrl
|
||||
it[license] = task.license
|
||||
it[subject] = task.subject
|
||||
it[minAge] = task.minAge
|
||||
it[maxAge] = task.maxAge
|
||||
it[taskType] = task.taskType
|
||||
it[question] = task.question
|
||||
it[options] = task.options.joinToString(",")
|
||||
it[correctAnswer] = task.correctAnswer
|
||||
it[rewardMinutes] = task.rewardMinutes
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
println("FEHLER beim Upsert von Task ${task.id}: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
TasksTable.selectAll()
|
||||
.where { TasksTable.id eq taskId }
|
||||
.map { it[TasksTable.correctAnswer] to it[TasksTable.rewardMinutes] }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getScreentime(userId: String): Int {
|
||||
val redisKey = "screentime:$userId"
|
||||
|
||||
try {
|
||||
RedisFactory.getResource().use { jedis ->
|
||||
val cachedSeconds = jedis.get(redisKey)
|
||||
if (cachedSeconds != null) {
|
||||
println("DEBUG: Bildschirmzeit für $userId aus REDIS geladen: $cachedSeconds Sek")
|
||||
return cachedSeconds.toInt()
|
||||
}
|
||||
}
|
||||
} 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())
|
||||
println("DEBUG: Bildschirmzeit für $userId in REDIS gecacht.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Konnte Wert nicht in Redis speichern: ${e.message}")
|
||||
}
|
||||
return dbSeconds
|
||||
}
|
||||
|
||||
override suspend fun updateScreentime(
|
||||
userId: String,
|
||||
seconds: Int,
|
||||
) {
|
||||
DatabaseFactory.dbQuery {
|
||||
val rowsUpdated =
|
||||
ScreentimeTable.update({ ScreentimeTable.userId eq userId }) {
|
||||
it[remainingSeconds] = seconds
|
||||
}
|
||||
if (rowsUpdated == 0) {
|
||||
ScreentimeTable.insert {
|
||||
it[ScreentimeTable.userId] = userId
|
||||
it[ScreentimeTable.remainingSeconds] = seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
val redisKey = "screentime:$userId"
|
||||
try {
|
||||
RedisFactory.getResource().use { jedis ->
|
||||
jedis.setex(redisKey, 3600, seconds.toString())
|
||||
println("DEBUG: Bildschirmzeit für $userId in REDIS aktualisiert auf $seconds Sek")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Konnte Redis nicht aktualisieren: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByEmail(email: String): User? =
|
||||
DatabaseFactory.dbQuery {
|
||||
UserTable.selectAll()
|
||||
.where { UserTable.email eq email }
|
||||
.map {
|
||||
User(
|
||||
id = it[UserTable.id],
|
||||
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 BCrypt.checkpw(password, passwordHash)
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryKidioRepository : KidioRepository {
|
||||
private val tasks =
|
||||
mutableListOf(
|
||||
ImportedTask(
|
||||
id = "t1",
|
||||
externalSource = "manual",
|
||||
subject = "Mathematik",
|
||||
minAge = 6,
|
||||
maxAge = 10,
|
||||
taskType = "MULTIPLE_CHOICE",
|
||||
question = "Was ist 12 + 15?",
|
||||
options = listOf("25", "27", "30", "22"),
|
||||
correctAnswer = "27",
|
||||
rewardMinutes = 10,
|
||||
license = "CC BY-SA 4.0",
|
||||
sourceUrl = "local",
|
||||
),
|
||||
ImportedTask(
|
||||
id = "t2",
|
||||
externalSource = "manual",
|
||||
subject = "Mathematik",
|
||||
minAge = 7,
|
||||
maxAge = 12,
|
||||
taskType = "MULTIPLE_CHOICE",
|
||||
question = "Was ist 8 x 7?",
|
||||
options = listOf("54", "56", "64", "48"),
|
||||
correctAnswer = "56",
|
||||
rewardMinutes = 15,
|
||||
license = "CC BY-SA 4.0",
|
||||
sourceUrl = "local",
|
||||
),
|
||||
)
|
||||
private val screentimes =
|
||||
mutableMapOf(
|
||||
"default_user" to 1800,
|
||||
)
|
||||
private val users = mutableListOf<User>()
|
||||
|
||||
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||
|
||||
override suspend fun getTasks(
|
||||
subject: String?,
|
||||
age: Int?,
|
||||
limit: Int,
|
||||
): List<Task> {
|
||||
val safeLimit = if (limit <= 0) 10 else limit
|
||||
return tasks.filter { task ->
|
||||
val matchSubject = subject.isNullOrBlank() || task.subject.equals(subject.trim(), ignoreCase = true)
|
||||
val matchAge = age == null || (task.minAge <= age && task.maxAge >= age)
|
||||
matchSubject && matchAge
|
||||
}.take(safeLimit).map {
|
||||
Task(
|
||||
id = it.id,
|
||||
subject = it.subject,
|
||||
minAge = it.minAge,
|
||||
maxAge = it.maxAge,
|
||||
question = it.question,
|
||||
options = it.options,
|
||||
rewardMinutes = it.rewardMinutes,
|
||||
license = it.license,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserById(userId: String): User? {
|
||||
return users.find { it.id == userId }
|
||||
}
|
||||
|
||||
// NEU: Implementierung für InMemory (Mock-Daten)
|
||||
override suspend fun getExistingTaskIds(externalSource: String): Set<String> {
|
||||
return tasks
|
||||
.filter { it.externalSource == externalSource }
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override suspend fun upsertTask(task: ImportedTask): Boolean {
|
||||
tasks.removeIf { it.id == task.id }
|
||||
tasks.add(task)
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? {
|
||||
val task = tasks.find { it.id == taskId } ?: return null
|
||||
return task.correctAnswer to task.rewardMinutes
|
||||
}
|
||||
|
||||
override suspend fun getScreentime(userId: String): Int = screentimes[userId] ?: 1800
|
||||
|
||||
override suspend fun updateScreentime(
|
||||
userId: String,
|
||||
seconds: Int,
|
||||
) {
|
||||
screentimes[userId] = seconds
|
||||
}
|
||||
|
||||
override suspend fun findByEmail(email: String): User? {
|
||||
return users.find { it.email == email }
|
||||
}
|
||||
|
||||
override suspend fun verifyPassword(
|
||||
password: String,
|
||||
passwordHash: String,
|
||||
): Boolean {
|
||||
return BCrypt.checkpw(password, passwordHash)
|
||||
}
|
||||
|
||||
override suspend fun registerUser(user: User): Boolean {
|
||||
users.add(user)
|
||||
screentimes[user.id] = 1800
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue