Merge pull request 'fixes for jwt token (now 30 days) screentime sync and redis' (#7) from feat/fixes-for-jwt-and-screentime into main
Reviewed-on: https://owgit.duckdns.org/DrunkenPinguin/LernApp/pulls/7 Reviewed-by: mt <aldemir.mt@icloud.com>
This commit is contained in:
commit
82b29f9d6e
|
|
@ -78,7 +78,7 @@ class ExposedKidioRepository : KidioRepository {
|
|||
val dbSeconds = DatabaseFactory.dbQuery {
|
||||
ScreentimeTable.selectAll()
|
||||
.where { ScreentimeTable.userId eq userId }
|
||||
.map { it[ScreentimeTable.remainingSeconds] } // Korrigiert: Abfrage der Sekunden!
|
||||
.map { it[ScreentimeTable.remainingSeconds] }
|
||||
.singleOrNull() ?: 1800
|
||||
}
|
||||
|
||||
|
|
@ -93,17 +93,28 @@ class ExposedKidioRepository : KidioRepository {
|
|||
return dbSeconds
|
||||
}
|
||||
|
||||
override suspend fun updateScreentime(userId: String, seconds: Int) = DatabaseFactory.dbQuery {
|
||||
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 // Korrigiert: Wert wird wieder mitgegeben!
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.oliver.kidio.backend.security.JwtService
|
|||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.auth.jwt.JWTPrincipal
|
||||
import io.ktor.server.auth.principal
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
|
|
@ -91,9 +90,8 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
|
|||
// 2. Antwort auf eine Aufgabe überprüfen
|
||||
post("/api/v1/tasks/verify") {
|
||||
val request = call.receive<TaskVerifyRequest>()
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val userId = principal?.payload?.getClaim("userId")?.asString()
|
||||
|
||||
val principal = call.principal<UserPrincipal>()
|
||||
val userId = principal?.userId
|
||||
if (userId == null){
|
||||
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiges oder abgelaufenes Token"))
|
||||
return@post
|
||||
|
|
@ -140,11 +138,15 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
|
|||
post("/api/v1/screentime/sync") {
|
||||
val request = call.receive<SyncRequest>()
|
||||
|
||||
val currentSeconds = repository.getScreentime(request.userId)
|
||||
// Wir holen die echte, fälschungssichere userId aus dem Login-Token:
|
||||
val principal = call.principal<UserPrincipal>()
|
||||
val userId = principal?.userId ?: request.userId // Fallback auf request.userId, falls token noch fehlt
|
||||
|
||||
val currentSeconds = repository.getScreentime(userId)
|
||||
var newSeconds = currentSeconds - request.usedSecondsSinceLastSync
|
||||
if (newSeconds < 0) newSeconds = 0
|
||||
|
||||
repository.updateScreentime(request.userId, newSeconds)
|
||||
repository.updateScreentime(userId, newSeconds)
|
||||
|
||||
call.respond(
|
||||
SyncResponse(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class JwtService(
|
|||
private val secret: String = System.getenv("JWT_SECRET") ?: "super-geheimes-secret-fuer-dev",
|
||||
private val issuer: String = "https://kidio.oliver.com/",
|
||||
private val audience: String = "kidio-users",
|
||||
private val expirationInMs: Long = 36_000_000 // 10 Stunden
|
||||
private val expirationInMs: Long = java.util.concurrent.TimeUnit.DAYS.toMillis(30) // 30 Tage
|
||||
) {
|
||||
|
||||
val verifier: JWTVerifier = JWT
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
package com.oliver.kidio.backend
|
||||
|
||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||
import com.oliver.kidio.backend.plugins.configureRouting
|
||||
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||
import com.oliver.kidio.backend.security.JwtService
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.testing.*
|
||||
import com.oliver.kidio.backend.plugins.configureRouting
|
||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||
import io.ktor.server.testing.*
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlin.test.*
|
||||
|
||||
|
|
@ -16,11 +18,15 @@ class ServerTest {
|
|||
|
||||
@Test
|
||||
fun `test health endpoint`() = testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
this.install(ServerContentNegotiation) {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureRouting(InMemoryKidioRepository())
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
val response = client.get("/api/v1/health")
|
||||
|
|
@ -29,16 +35,87 @@ class ServerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `test tasks endpoint and verify`() = testApplication {
|
||||
fun `test protected endpoints reject unauthorized requests`() = testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
this.install(ServerContentNegotiation) {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureRouting(InMemoryKidioRepository())
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
// 1. Get Tasks
|
||||
// Ohne Token muss 401 Unauthorized kommen
|
||||
val tasksResponse = client.get("/api/v1/tasks")
|
||||
assertEquals(HttpStatusCode.Unauthorized, tasksResponse.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test registration and login flow`() = testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
// 1. Registrierung
|
||||
val registerResponse = client.post("/api/v1/auth/register") {
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody(
|
||||
"""{
|
||||
"email": "test@family.de",
|
||||
"passwordPlain": "strengGeheim1",
|
||||
"familyName": "Familie Schmidt",
|
||||
"childAge": 8,
|
||||
"parentPin": "4321"
|
||||
}"""
|
||||
)
|
||||
}
|
||||
assertEquals(HttpStatusCode.Created, registerResponse.status)
|
||||
assertTrue(registerResponse.bodyAsText().contains("token"))
|
||||
|
||||
// 2. Login mit denselben Daten
|
||||
val loginResponse = client.post("/api/v1/auth/login") {
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody(
|
||||
"""{
|
||||
"email": "test@family.de",
|
||||
"passwordPlain": "strengGeheim1"
|
||||
}"""
|
||||
)
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, loginResponse.status)
|
||||
val loginJson = Json.decodeFromString<AuthResponse>(loginResponse.bodyAsText())
|
||||
assertNotNull(loginJson.token)
|
||||
assertEquals("Familie Schmidt", loginJson.familyName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tasks endpoint and verify with jwt`() = testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
val token = jwtService.generateToken("default_user")
|
||||
|
||||
// 1. Get Tasks mit Token
|
||||
val tasksResponse = client.get("/api/v1/tasks") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, tasksResponse.status)
|
||||
val tasksJson = Json.decodeFromString<List<Task>>(tasksResponse.bodyAsText())
|
||||
assertEquals(2, tasksJson.size)
|
||||
|
|
@ -47,6 +124,7 @@ class ServerTest {
|
|||
|
||||
// 2. Verify Task correct answer
|
||||
val verifyResponse = client.post("/api/v1/tasks/verify") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"taskId":"t1","selectedAnswer":"27"}""")
|
||||
}
|
||||
|
|
@ -57,6 +135,7 @@ class ServerTest {
|
|||
|
||||
// 3. Verify Task incorrect answer
|
||||
val verifyWrongResponse = client.post("/api/v1/tasks/verify") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"taskId":"t1","selectedAnswer":"99"}""")
|
||||
}
|
||||
|
|
@ -67,16 +146,23 @@ class ServerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `test screentime sync`() = testApplication {
|
||||
fun `test screentime sync deducts time repeatedly`() = testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
this.install(ServerContentNegotiation) {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureRouting(InMemoryKidioRepository())
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
// Sync first time (user default_user has 1800s seeded)
|
||||
val token = jwtService.generateToken("default_user")
|
||||
|
||||
// 1. Sync: 200 Sekunden abziehen (1800 -> 1600)
|
||||
val syncResponse1 = client.post("/api/v1/screentime/sync") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":200}""")
|
||||
}
|
||||
|
|
@ -85,14 +171,26 @@ class ServerTest {
|
|||
assertEquals(1600, syncJson1.remainingSeconds)
|
||||
assertFalse(syncJson1.isBlocked)
|
||||
|
||||
// Sync and block
|
||||
// 2. Erneuter Sync: Weitere 300 Sekunden abziehen (1600 -> 1300) - darf nicht auf 1800 zurückspringen!
|
||||
val syncResponse2 = client.post("/api/v1/screentime/sync") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":2000}""")
|
||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":300}""")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, syncResponse2.status)
|
||||
val syncJson2 = Json.decodeFromString<SyncResponse>(syncResponse2.bodyAsText())
|
||||
assertEquals(0, syncJson2.remainingSeconds)
|
||||
assertTrue(syncJson2.isBlocked)
|
||||
assertEquals(1300, syncJson2.remainingSeconds)
|
||||
assertFalse(syncJson2.isBlocked)
|
||||
|
||||
// 3. Sync bis zur Sperre (weitere 1500 Sekunden abziehen -> 0)
|
||||
val syncResponse3 = client.post("/api/v1/screentime/sync") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":1500}""")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, syncResponse3.status)
|
||||
val syncJson3 = Json.decodeFromString<SyncResponse>(syncResponse3.bodyAsText())
|
||||
assertEquals(0, syncJson3.remainingSeconds)
|
||||
assertTrue(syncJson3.isBlocked)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue