Added authentication #4
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
||||
// 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
|
||||
// redis
|
||||
implementation("redis.clients:jedis:5.1.2")
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -82,4 +93,6 @@ fun Application.configureRouting(repository: KidioRepository) {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
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.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
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue