Compare commits

...

3 commits

Author SHA1 Message Date
mt de33790206 Merge pull request 'Added authentication' (#4) from feat/authentication into main
Reviewed-on: https://owgit.duckdns.org/DrunkenPinguin/LernApp/pulls/4
Reviewed-by: DrunkenPinguin <wallisch.o@freenet.de>
2026-09-04 10:57:33 +00:00
aldemirm 57e91c6742 refactored username 2026-09-04 12:56:01 +02:00
aldemirm a1c01b5112 Added authentication 2026-09-04 12:54:10 +02:00
11 changed files with 221 additions and 68 deletions

View file

@ -19,20 +19,26 @@ dependencies {
implementation(ktorLibs.server.config.yaml)
implementation(ktorLibs.server.core)
implementation(ktorLibs.server.netty)
// JWT Authentication
implementation("io.ktor:ktor-server-auth")
implementation("io.ktor:ktor-server-auth-jwt")
implementation(libs.logback.classic)
// JSON-Unterstützung und Serialization
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
// postgres
implementation("org.postgresql:postgresql:42.7.3")
implementation("org.jetbrains.exposed:exposed-core:0.50.0")
implementation("org.jetbrains.exposed:exposed-dao:0.50.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.50.0")
testImplementation(kotlin("test"))
testImplementation(ktorLibs.server.testHost)
// redis
implementation("redis.clients:jedis:5.1.2")
}

View file

@ -24,6 +24,14 @@ object ScreentimeTable : Table("user_screentime") {
override val primaryKey = PrimaryKey(userId)
}
object UserTable : Table("users") {
val id = varchar("id", 36)
val username = varchar("username", 50).index(isUnique = true)
val passwordHash = varchar("password_hash", 255)
override val primaryKey = PrimaryKey(id)
}
object DatabaseFactory {
fun init(config: ApplicationConfig) {
val driverClassName = config.propertyOrNull("storage.driverClassName")?.getString() ?: "org.postgresql.Driver"
@ -40,7 +48,7 @@ object DatabaseFactory {
?: config.propertyOrNull("storage.database")?.getString()
?: "kidio_db"
val username = System.getenv("DB_USER")
val dbUsername = System.getenv("DB_USER")
?: config.propertyOrNull("storage.username")?.getString()
?: "postgres"
@ -53,12 +61,12 @@ object DatabaseFactory {
val database = Database.connect(
url = jdbcURL,
driver = driverClassName,
user = username,
user = dbUsername,
password = password
)
transaction(database) {
SchemaUtils.create(TasksTable, ScreentimeTable)
SchemaUtils.create(TasksTable, ScreentimeTable, UserTable)
// Standard aufgaben, sollte es keine mehr geben
if (TasksTable.selectAll().empty()) {
@ -86,6 +94,16 @@ object DatabaseFactory {
it[parentPin] = "1234"
}
}
// Create a standard user if the Users table is empty.
if (UserTable.selectAll().empty()) {
UserTable.insert {
// ID automatische generierung
it[id] = "default_user"
it[username] = "default_user"
it[passwordHash] = "1234"
}
}
}
}

View file

@ -33,6 +33,18 @@ data class SyncRequest(
val usedSecondsSinceLastSync: Int
)
@Serializable
data class LoginRequest(
val userId: String
)
@Serializable
data class User(
val id: String,
val username: String,
val passwordHash: String
)
@Serializable
data class SyncResponse(
val remainingSeconds: Int,

View file

@ -1,10 +1,12 @@
package com.oliver.kidio.backend.domain.repository
import com.oliver.kidio.backend.Task
import com.oliver.kidio.backend.User
import com.oliver.kidio.backend.data.database.DatabaseFactory
import com.oliver.kidio.backend.data.database.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.*
interface KidioRepository {
@ -12,6 +14,9 @@ interface KidioRepository {
suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>?
suspend fun getScreentime(userId: String): Int
suspend fun updateScreentime(userId: String, seconds: Int)
suspend fun findByUsername(username: String): User?
suspend fun verifyPassword(password: String, passwordHash: String): Boolean
}
class ExposedKidioRepository : KidioRepository {
@ -34,7 +39,7 @@ class ExposedKidioRepository : KidioRepository {
}
override suspend fun getScreentime(userId: String): Int {
var redisKey = "screentime:$userId"
val redisKey = "screentime:$userId"
try {
RedisFactory.getResource().use { jedis ->
@ -76,6 +81,23 @@ class ExposedKidioRepository : KidioRepository {
}
}
}
override suspend fun findByUsername(username: String): User? = DatabaseFactory.dbQuery {
UserTable.selectAll()
.where { UserTable.username eq username }
.map {
User(
id = it[UserTable.id],
username = it[UserTable.username],
passwordHash = it[UserTable.passwordHash]
)
}
.singleOrNull()
}
override suspend fun verifyPassword(password: String, passwordHash: String): Boolean {
return password == passwordHash
}
}
class InMemoryKidioRepository : KidioRepository {
@ -91,6 +113,10 @@ class InMemoryKidioRepository : KidioRepository {
"default_user" to 1800
)
private val users = mutableListOf(
User(id = "1", username = "max", passwordHash = "geheim123")
)
override suspend fun getAllTasks(): List<Task> = tasks
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? = answers[taskId]
@ -100,4 +126,12 @@ class InMemoryKidioRepository : KidioRepository {
override suspend fun updateScreentime(userId: String, seconds: Int) {
screentimes[userId] = seconds
}
override suspend fun findByUsername(username: String): User? {
return users.find { it.username == username }
}
override suspend fun verifyPassword(password: String, passwordHash: String): Boolean {
return password == passwordHash
}
}

View file

@ -2,12 +2,14 @@ package com.oliver.kidio.backend.plugins
import com.oliver.kidio.backend.*
import com.oliver.kidio.backend.domain.repository.KidioRepository
import com.oliver.kidio.backend.security.JwtService
import io.ktor.server.application.*
import io.ktor.server.auth.authenticate
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureRouting(repository: KidioRepository) {
fun Application.configureRouting(repository: KidioRepository, jwtService : JwtService) {
routing {
// Health-Check
@ -15,6 +17,15 @@ fun Application.configureRouting(repository: KidioRepository) {
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
}
post ("/api/v1/auth/login" ) {
val loginRequest = call.receive<LoginRequest>()
val token = jwtService.generateToken(userId = loginRequest.userId)
call.respond(mapOf("token" to token))
}
authenticate("auth-jwt") {
// 1. Alle verfügbaren Aufgaben abrufen
get("/api/v1/tasks") {
val tasks = repository.getAllTasks()
@ -83,3 +94,5 @@ fun Application.configureRouting(repository: KidioRepository) {
}
}
}
}

View file

@ -0,0 +1,29 @@
package com.oliver.kidio.backend.plugins
import com.oliver.kidio.backend.security.JwtService
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
/**
* Repräsentiert den authentifizierten Benutzer im Request-Kontext.
*/
data class UserPrincipal(val userId: String) : Principal
fun Application.configureSecurity(jwtService: JwtService) {
install(Authentication) {
jwt("auth-jwt") {
realm = "Kidio Backend"
verifier(jwtService.verifier)
validate { credential ->
val userId = credential.payload.getClaim("userId").asString()
if (!userId.isNullOrEmpty()) {
UserPrincipal(userId)
} else {
null
}
}
}
}
}

View file

@ -0,0 +1,32 @@
package com.oliver.kidio.backend.security
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import java.util.Date
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
) {
val verifier: JWTVerifier = JWT
.require(Algorithm.HMAC256(secret))
.withAudience(audience)
.withIssuer(issuer)
.build()
/**
* Generiert ein JWT-Token für einen bestimmten Benutzer.
*/
fun generateToken(userId: String): String {
return JWT.create()
.withAudience(audience)
.withIssuer(issuer)
.withClaim("userId", userId)
.withExpiresAt(Date(System.currentTimeMillis() + expirationInMs))
.sign(Algorithm.HMAC256(secret))
}
}

View file

@ -2,6 +2,8 @@ import com.oliver.kidio.backend.plugins.configureRouting
import com.oliver.kidio.backend.data.database.DatabaseFactory
import com.oliver.kidio.backend.data.database.RedisFactory
import com.oliver.kidio.backend.domain.repository.ExposedKidioRepository
import com.oliver.kidio.backend.plugins.configureSecurity
import com.oliver.kidio.backend.security.JwtService
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
@ -16,6 +18,13 @@ fun Application.module() {
json()
}
// Ruft die Funktion aus Routing.kt auf mit dem echten ExposedRepository
configureRouting(ExposedKidioRepository())
val jwtService = JwtService()
val repository = ExposedKidioRepository()
configureSecurity(jwtService)
configureRouting(
repository = repository,
jwtService = jwtService
)
}

View file

@ -10,5 +10,5 @@ storage:
host: "82.165.11.162"
port: "5432"
database: "kidio_db"
username: "postgres"
username: "kidio_admin"
password: "learning"