Added authentication
This commit is contained in:
parent
ad90c4c7d7
commit
a1c01b5112
Binary file not shown.
Binary file not shown.
|
|
@ -19,20 +19,26 @@ dependencies {
|
||||||
implementation(ktorLibs.server.config.yaml)
|
implementation(ktorLibs.server.config.yaml)
|
||||||
implementation(ktorLibs.server.core)
|
implementation(ktorLibs.server.core)
|
||||||
implementation(ktorLibs.server.netty)
|
implementation(ktorLibs.server.netty)
|
||||||
|
|
||||||
|
// JWT Authentication
|
||||||
|
implementation("io.ktor:ktor-server-auth")
|
||||||
|
implementation("io.ktor:ktor-server-auth-jwt")
|
||||||
|
|
||||||
implementation(libs.logback.classic)
|
implementation(libs.logback.classic)
|
||||||
|
|
||||||
// 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")
|
||||||
//postgres
|
|
||||||
|
// postgres
|
||||||
implementation("org.postgresql:postgresql:42.7.3")
|
implementation("org.postgresql:postgresql:42.7.3")
|
||||||
implementation("org.jetbrains.exposed:exposed-core:0.50.0")
|
implementation("org.jetbrains.exposed:exposed-core:0.50.0")
|
||||||
implementation("org.jetbrains.exposed:exposed-dao:0.50.0")
|
implementation("org.jetbrains.exposed:exposed-dao:0.50.0")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:0.50.0")
|
implementation("org.jetbrains.exposed:exposed-jdbc:0.50.0")
|
||||||
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(ktorLibs.server.testHost)
|
testImplementation(ktorLibs.server.testHost)
|
||||||
|
|
||||||
//redis
|
// redis
|
||||||
implementation("redis.clients:jedis:5.1.2")
|
implementation("redis.clients:jedis:5.1.2")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -24,6 +24,14 @@ object ScreentimeTable : Table("user_screentime") {
|
||||||
override val primaryKey = PrimaryKey(userId)
|
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 {
|
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"
|
||||||
|
|
@ -40,8 +48,8 @@ object DatabaseFactory {
|
||||||
?: config.propertyOrNull("storage.database")?.getString()
|
?: config.propertyOrNull("storage.database")?.getString()
|
||||||
?: "kidio_db"
|
?: "kidio_db"
|
||||||
|
|
||||||
val username = System.getenv("DB_USER")
|
val dbUsername = System.getenv("DB_USER")
|
||||||
?: config.propertyOrNull("storage.username")?.getString()
|
?: config.propertyOrNull("storage.username")?.getString()
|
||||||
?: "postgres"
|
?: "postgres"
|
||||||
|
|
||||||
val password = System.getenv("DB_PASSWORD")
|
val password = System.getenv("DB_PASSWORD")
|
||||||
|
|
@ -53,13 +61,13 @@ object DatabaseFactory {
|
||||||
val database = Database.connect(
|
val database = Database.connect(
|
||||||
url = jdbcURL,
|
url = jdbcURL,
|
||||||
driver = driverClassName,
|
driver = driverClassName,
|
||||||
user = username,
|
user = dbUsername,
|
||||||
password = password
|
password = password
|
||||||
)
|
)
|
||||||
|
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
SchemaUtils.create(TasksTable, ScreentimeTable)
|
SchemaUtils.create(TasksTable, ScreentimeTable, UserTable)
|
||||||
|
|
||||||
// Standard aufgaben, sollte es keine mehr geben
|
// Standard aufgaben, sollte es keine mehr geben
|
||||||
if (TasksTable.selectAll().empty()) {
|
if (TasksTable.selectAll().empty()) {
|
||||||
TasksTable.insert {
|
TasksTable.insert {
|
||||||
|
|
@ -86,6 +94,16 @@ object DatabaseFactory {
|
||||||
it[parentPin] = "1234"
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package com.oliver.kidio.backend.domain.repository
|
package com.oliver.kidio.backend.domain.repository
|
||||||
|
|
||||||
import com.oliver.kidio.backend.Task
|
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.DatabaseFactory
|
||||||
import com.oliver.kidio.backend.data.database.RedisFactory
|
import com.oliver.kidio.backend.data.database.RedisFactory
|
||||||
import com.oliver.kidio.backend.data.database.ScreentimeTable
|
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 org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
|
||||||
interface KidioRepository {
|
interface KidioRepository {
|
||||||
|
|
@ -12,6 +14,9 @@ interface KidioRepository {
|
||||||
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 findByUsername(username: String): User?
|
||||||
|
suspend fun verifyPassword(password: String, passwordHash: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExposedKidioRepository : KidioRepository {
|
class ExposedKidioRepository : KidioRepository {
|
||||||
|
|
@ -34,7 +39,7 @@ class ExposedKidioRepository : KidioRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getScreentime(userId: String): Int {
|
override suspend fun getScreentime(userId: String): Int {
|
||||||
var redisKey = "screentime:$userId"
|
val redisKey = "screentime:$userId"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RedisFactory.getResource().use { jedis ->
|
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 {
|
class InMemoryKidioRepository : KidioRepository {
|
||||||
|
|
@ -91,6 +113,10 @@ class InMemoryKidioRepository : KidioRepository {
|
||||||
"default_user" to 1800
|
"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 getAllTasks(): List<Task> = tasks
|
||||||
|
|
||||||
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? = answers[taskId]
|
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) {
|
override suspend fun updateScreentime(userId: String, seconds: Int) {
|
||||||
screentimes[userId] = seconds
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ 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 io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
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.*
|
||||||
|
|
||||||
fun Application.configureRouting(repository: KidioRepository) {
|
fun Application.configureRouting(repository: KidioRepository, jwtService : JwtService) {
|
||||||
routing {
|
routing {
|
||||||
|
|
||||||
// Health-Check
|
// Health-Check
|
||||||
|
|
@ -15,71 +17,82 @@ fun Application.configureRouting(repository: KidioRepository) {
|
||||||
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
|
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Alle verfügbaren Aufgaben abrufen
|
post ("/api/v1/auth/login" ) {
|
||||||
get("/api/v1/tasks") {
|
val loginRequest = call.receive<LoginRequest>()
|
||||||
val tasks = repository.getAllTasks()
|
|
||||||
call.respond(tasks)
|
val token = jwtService.generateToken(userId = loginRequest.userId)
|
||||||
|
call.respond(mapOf("token" to token))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Antwort auf eine Aufgabe überprüfen
|
authenticate("auth-jwt") {
|
||||||
post("/api/v1/tasks/verify") {
|
|
||||||
val request = call.receive<TaskVerifyRequest>()
|
|
||||||
|
|
||||||
val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
|
// 1. Alle verfügbaren Aufgaben abrufen
|
||||||
|
get("/api/v1/tasks") {
|
||||||
if (taskInfo == null) {
|
val tasks = repository.getAllTasks()
|
||||||
call.respond(
|
call.respond(tasks)
|
||||||
TaskVerifyResponse(
|
|
||||||
isCorrect = false,
|
|
||||||
earnedMinutes = 0,
|
|
||||||
message = "Aufgabe nicht gefunden!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val (correctAnswer, reward) = taskInfo
|
// 2. Antwort auf eine Aufgabe überprüfen
|
||||||
val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim()
|
post("/api/v1/tasks/verify") {
|
||||||
|
val request = call.receive<TaskVerifyRequest>()
|
||||||
|
|
||||||
if (isCorrect) {
|
val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
|
||||||
val currentScreentime = repository.getScreentime("default_user")
|
|
||||||
val newSeconds = currentScreentime + (reward * 60)
|
|
||||||
repository.updateScreentime("default_user", newSeconds)
|
|
||||||
|
|
||||||
call.respond(
|
if (taskInfo == null) {
|
||||||
TaskVerifyResponse(
|
call.respond(
|
||||||
isCorrect = true,
|
TaskVerifyResponse(
|
||||||
earnedMinutes = reward,
|
isCorrect = false,
|
||||||
message = "Super gemacht! Du hast $reward Minuten gewonnen."
|
earnedMinutes = 0,
|
||||||
|
message = "Aufgabe nicht gefunden!"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
return@post
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
post("/api/v1/screentime/sync") {
|
||||||
|
val request = call.receive<SyncRequest>()
|
||||||
|
|
||||||
|
val currentSeconds = repository.getScreentime(request.userId)
|
||||||
|
var newSeconds = currentSeconds - request.usedSecondsSinceLastSync
|
||||||
|
if (newSeconds < 0) newSeconds = 0
|
||||||
|
|
||||||
|
repository.updateScreentime(request.userId, newSeconds)
|
||||||
|
|
||||||
call.respond(
|
call.respond(
|
||||||
TaskVerifyResponse(
|
SyncResponse(
|
||||||
isCorrect = false,
|
remainingSeconds = newSeconds,
|
||||||
earnedMinutes = 0,
|
isBlocked = newSeconds <= 0
|
||||||
message = "Schade, das war leider falsch. Versuche es nochmal!"
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Bildschirmzeit synchronisieren
|
|
||||||
post("/api/v1/screentime/sync") {
|
|
||||||
val request = call.receive<SyncRequest>()
|
|
||||||
|
|
||||||
val currentSeconds = repository.getScreentime(request.userId)
|
|
||||||
var newSeconds = currentSeconds - request.usedSecondsSinceLastSync
|
|
||||||
if (newSeconds < 0) newSeconds = 0
|
|
||||||
|
|
||||||
repository.updateScreentime(request.userId, newSeconds)
|
|
||||||
|
|
||||||
call.respond(
|
|
||||||
SyncResponse(
|
|
||||||
remainingSeconds = newSeconds,
|
|
||||||
isBlocked = newSeconds <= 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
29
src/main/kotlin/com/oliver/kidio/backend/plugins/Security.kt
Normal file
29
src/main/kotlin/com/oliver/kidio/backend/plugins/Security.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.DatabaseFactory
|
||||||
import com.oliver.kidio.backend.data.database.RedisFactory
|
import com.oliver.kidio.backend.data.database.RedisFactory
|
||||||
import com.oliver.kidio.backend.domain.repository.ExposedKidioRepository
|
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.serialization.kotlinx.json.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.plugins.contentnegotiation.*
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
|
@ -16,6 +18,13 @@ fun Application.module() {
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ruft die Funktion aus Routing.kt auf mit dem echten ExposedRepository
|
val jwtService = JwtService()
|
||||||
configureRouting(ExposedKidioRepository())
|
val repository = ExposedKidioRepository()
|
||||||
}
|
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
|
||||||
|
configureRouting(
|
||||||
|
repository = repository,
|
||||||
|
jwtService = jwtService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue