diff --git a/books/src/commonMain/kotlin/net/keinekekse/app/books/Version.kt b/books/src/commonMain/kotlin/net/keinekekse/app/books/Version.kt index 2e0a926..d4e6d6f 100644 --- a/books/src/commonMain/kotlin/net/keinekekse/app/books/Version.kt +++ b/books/src/commonMain/kotlin/net/keinekekse/app/books/Version.kt @@ -4,13 +4,13 @@ import net.keinekekse.version.Version object VERSION: Version { override val version = "1.0.0" - override val fullVersion = "1.0.0+build.221" + override val fullVersion = "1.0.0+build.222" override val major = 1 override val minor = 0 override val patch = 0 override val preRelease = "" - override val buildMetadata = "build.221" - override val buildDate = "2026-02-06T22:31:36.863331Z" + override val buildMetadata = "build.222" + override val buildDate = "2026-02-07T16:26:28.842311Z" override val displayName = "Books App" override val copyright = "Copyright (c) 2026 Hartmut Bauer" override val name = "books" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8768efe..705c369 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,19 +8,20 @@ composeAppVersion = "1.0.0" androidx-lifecycle = "2.9.6" composeHotReload = "1.0.0" composeMultiplatform = "1.10.0" +exposed = "1.0.0" +hikari = "7.0.2" junit = "4.13.2" kotlin = "2.3.0" kotlin-wrappers-css = "2025.6.4" kotlinx-coroutines = "1.10.2" kotlinx-html = "0.12.0" koin-bom = "4.2.0-RC1" +kotlinLogging = "7.0.14" koin-plugin = "0.3.0" ktor = "3.4.0" -logback = "1.5.26" -kotlinLogging = "7.0.14" +logback = "1.5.28" material3 = "1.10.0-alpha05" -settings = "1.3.0" -dataStore = "1.2.0" +postgres = "42.7.9" uiDesktop = "1.7.0" [libraries] @@ -55,6 +56,9 @@ ktor-serverHtmlBuilder = { module = "io.ktor:ktor-server-html-builder", version. ktor-serverHtmx = { module = "io.ktor:ktor-server-htmx", version.ref = "ktor" } ktor-htmxHtml = { module = "io.ktor:ktor-htmx-html", version.ref = "ktor" } ktor-serverContentNegotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } +ktor-server-host-common = { module = "io.ktor:ktor-server-host-common-jvm", version.ref = "ktor" } +ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages-jvm", version.ref = "ktor" } + ktor-serverTestHost = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor" } ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-clientAuth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } @@ -84,10 +88,11 @@ koin-test = { module = "io.insert-koin:koin-test" } #koin-compose-viewmodel-navigation3 = { module = "io.insert-koin:koin-compose-viewmodel-navigation3" } # ---- -# Settings vorerst ohne externe Hilfe -#multiplatformSettings = { module = "com.russhwolf:multiplatform-settings", version.ref = "settings" } -#androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "dataStore" } -#androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "dataStore" } +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgres" } +exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } +exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } +hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } [bundles] kotlin-compose = [ diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 6f7bb2c..39b0f88 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation(libs.ktor.serverAuthJwt) implementation(libs.ktor.serverContentNegotiation) implementation(libs.ktor.serializationJson) + implementation(libs.ktor.server.host.common) + implementation(libs.ktor.server.status.pages) implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.logger.slf4j) @@ -38,6 +40,12 @@ dependencies { implementation(libs.koin.ktor) // implementation("io.insert-koin:koin-logger-slf4j:4.1.2-Beta1") + implementation(libs.postgresql) + implementation(libs.exposed.core) + implementation(libs.exposed.jdbc) + implementation(libs.exposed.dao) + implementation(libs.hikaricp) + testImplementation(libs.ktor.serverTestHost) testImplementation(libs.kotlin.testJunit) } diff --git a/server/buildnumber.properties b/server/buildnumber.properties index 3d9ac76..2b782f8 100644 --- a/server/buildnumber.properties +++ b/server/buildnumber.properties @@ -1,3 +1,3 @@ #auto generated -#Sat Feb 07 20:02:00 CET 2026 -build.number=27 +#Sun Feb 08 01:38:22 CET 2026 +build.number=35 diff --git a/server/src/main/kotlin/net/keinekekse/app/Application.kt b/server/src/main/kotlin/net/keinekekse/app/Application.kt index 7dac0ab..7cfa48f 100644 --- a/server/src/main/kotlin/net/keinekekse/app/Application.kt +++ b/server/src/main/kotlin/net/keinekekse/app/Application.kt @@ -3,9 +3,13 @@ package net.keinekekse.app import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.server.application.* import net.keinekekse.app.server.VERSION +import net.keinekekse.app.server.config.configureDatabase import net.keinekekse.app.server.config.configureKoin import net.keinekekse.app.server.config.configureRouting import net.keinekekse.app.server.config.configureSecurity +import net.keinekekse.app.server.testdb.db.BookTable +import org.jetbrains.exposed.v1.jdbc.SchemaUtils +import org.jetbrains.exposed.v1.jdbc.transactions.transaction private val logger = KotlinLogging.logger {} @@ -26,4 +30,10 @@ fun Application.module() { configureKoin() configureSecurity() configureRouting() + configureDatabase() + + // create if not exists + transaction { + SchemaUtils.create(BookTable) + } } diff --git a/server/src/main/kotlin/net/keinekekse/app/server/Version.kt b/server/src/main/kotlin/net/keinekekse/app/server/Version.kt index 7f083cc..75e31e7 100644 --- a/server/src/main/kotlin/net/keinekekse/app/server/Version.kt +++ b/server/src/main/kotlin/net/keinekekse/app/server/Version.kt @@ -3,14 +3,14 @@ package net.keinekekse.app.server import net.keinekekse.version.Version object VERSION: Version { - override val version = "0.1.0-beta.1+build.27" - override val fullVersion = "0.1.0-beta.1+build.27" + override val version = "0.1.0-beta.1+build.35" + override val fullVersion = "0.1.0-beta.1+build.35" override val major = 0 override val minor = 1 override val patch = 0 override val preRelease = "beta.1" - override val buildMetadata = "build.27" - override val buildDate = "2026-02-07T19:02:00.909145Z" + override val buildMetadata = "build.35" + override val buildDate = "2026-02-08T00:38:22.418116Z" override val displayName = "kAS" override val copyright = "Copyright (c) 2026 Hartmut Bauer" override val name = "server" diff --git a/server/src/main/kotlin/net/keinekekse/app/server/config/database.kt b/server/src/main/kotlin/net/keinekekse/app/server/config/database.kt new file mode 100644 index 0000000..526cee2 --- /dev/null +++ b/server/src/main/kotlin/net/keinekekse/app/server/config/database.kt @@ -0,0 +1,47 @@ +package net.keinekekse.app.server.config + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import io.ktor.server.application.* +import org.jetbrains.exposed.v1.core.DatabaseConfig +import org.jetbrains.exposed.v1.jdbc.Database + +fun Application.configureDatabase() { + + val config = HikariConfig().apply { +// jdbcUrl = "jdbc:mysql://$host:$port/$db" + dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" + username = getKeyFromConfiguration("postgres.user") + password = getKeyFromConfiguration("postgres.password") + + addDataSourceProperty("serverName", getKeyFromConfiguration("postgres.host", "localhost")) + addDataSourceProperty("portNumber", getKeyFromConfiguration("postgres.port", "5432")) + addDataSourceProperty( + "databaseName", + getKeyFromConfiguration("postgres.database", username) + ) + + maximumPoolSize = 6 + isReadOnly = false + transactionIsolation = "TRANSACTION_SERIALIZABLE" + } + + val dataSource = HikariDataSource(config) + + Database.connect( + datasource = dataSource, + databaseConfig = DatabaseConfig { + // set other parameters here + } + ) + +// Class.forName("org.postgresql.Driver") +// val host = getKeyFromConfiguration("postgres.host", "localhost") +// val port = getKeyFromConfiguration("postgres.port", "5432") +// val user = getKeyFromConfiguration("postgres.user") +// val password = getKeyFromConfiguration("postgres.password") +// val database = getKeyFromConfiguration("postgres.database", user) +// +// // "jdbc:postgresql://host:5432/database", +// return DriverManager.getConnection("jdbc:postgresql://$host:$port/$database", user, password) +} diff --git a/server/src/main/kotlin/net/keinekekse/app/server/config/getKeyFromConfiguration.kt b/server/src/main/kotlin/net/keinekekse/app/server/config/getKeyFromConfiguration.kt new file mode 100644 index 0000000..eb653cc --- /dev/null +++ b/server/src/main/kotlin/net/keinekekse/app/server/config/getKeyFromConfiguration.kt @@ -0,0 +1,10 @@ +package net.keinekekse.app.server.config + +import io.ktor.server.application.* + +fun Application.getKeyFromConfiguration(key: String, default: String? = null): String { + val value = environment.config.propertyOrNull(key)?.getString() ?: default + // ohne die Konfig-Params geht nix, hier ist nur noch Abbruch möglich + return value + ?: throw IllegalStateException("Configuration key '$key' not found, server start aborted.") +} diff --git a/server/src/main/kotlin/net/keinekekse/app/server/config/security.kt b/server/src/main/kotlin/net/keinekekse/app/server/config/security.kt index 0a5f3b3..339bcfa 100644 --- a/server/src/main/kotlin/net/keinekekse/app/server/config/security.kt +++ b/server/src/main/kotlin/net/keinekekse/app/server/config/security.kt @@ -6,7 +6,6 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* -import io.ktor.server.config.* import io.ktor.server.response.* import java.net.URI @@ -14,25 +13,13 @@ private val logger = KotlinLogging.logger {} fun Application.configureSecurity() { - fun getFromConfig(key: String): String { - try { - return environment.config.property("ktor.conf.$key").getString() - } catch (e: ApplicationConfigurationException) { - // ohne die Konfig-Params geht nix (und defaults sind hier sinnlos), hier ist nur noch Abbruch möglich - throw IllegalStateException( - "Configuration key '$key' not found, server start aborted.", - e - ) - } - } - // Kommunikation funktioniert nur im internen Containernetzwerk, // d.h. Container- bzw. Pod-Name + interner Port - val keycloakInternalUrl = getFromConfig("keycloakInternalUrl") - val keycloakUrl = getFromConfig("keycloakUrl") - val keycloakRealm = getFromConfig("keycloakRealm") - val keycloakClientId = getFromConfig("keycloakClientId") - val keycloakCertUrl = getFromConfig("keycloakCertUrl") + val keycloakInternalUrl = getKeyFromConfiguration("keycloak.internalUrl") + val keycloakUrl = getKeyFromConfiguration("keycloak.url") + val keycloakRealm = getKeyFromConfiguration("keycloak.realm") + val keycloakClientId = getKeyFromConfiguration("keycloak.clientId") + val keycloakCertUrl = getKeyFromConfiguration("keycloak.certUrl") val certUrl = "$keycloakInternalUrl/$keycloakRealm/$keycloakCertUrl" val issuer = "$keycloakUrl/$keycloakRealm" diff --git a/server/src/main/kotlin/net/keinekekse/app/server/testdb/db/BookDAO.kt b/server/src/main/kotlin/net/keinekekse/app/server/testdb/db/BookDAO.kt new file mode 100644 index 0000000..112682f --- /dev/null +++ b/server/src/main/kotlin/net/keinekekse/app/server/testdb/db/BookDAO.kt @@ -0,0 +1,25 @@ +package net.keinekekse.app.server.testdb.db + +import net.keinekekse.app.server.testdb.model.Book +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.LongIdTable +import org.jetbrains.exposed.v1.dao.LongEntity +import org.jetbrains.exposed.v1.dao.LongEntityClass + +object BookTable : LongIdTable("book") { + var title = varchar("title", 50) + var author = varchar("author", 50) + var series = varchar("series", 50) + var seriesIndex = integer("seriesIndex") +} + +class BookDAO(id: EntityID) : LongEntity(id) { + companion object : LongEntityClass(BookTable) + + var title by BookTable.title + var author by BookTable.author + var series by BookTable.series + var seriesIndex by BookTable.seriesIndex +} + +fun daoToModel(dao: BookDAO) = Book(dao.title, dao.author, dao.series, dao.seriesIndex) diff --git a/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/Book.kt b/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/Book.kt new file mode 100644 index 0000000..60aa41a --- /dev/null +++ b/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/Book.kt @@ -0,0 +1,11 @@ +package net.keinekekse.app.server.testdb.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Book( + var title: String = "", + var author: String = "", + var series: String = "", + var seriesIndex: Int = 0, +) diff --git a/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/BookRepository.kt b/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/BookRepository.kt new file mode 100644 index 0000000..7b5b913 --- /dev/null +++ b/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/BookRepository.kt @@ -0,0 +1,11 @@ +package net.keinekekse.app.server.testdb.model + +interface BookRepository { + suspend fun allBooks(): List + suspend fun bookById(id: Long): Book? + suspend fun booksBySeries(series: String): List + suspend fun bookByTitle(title: String): List + suspend fun bookByAuthor(author: String): List + suspend fun addTBook(book: Book) + suspend fun removeBook(id: Long): Boolean +} diff --git a/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/BookRepositoryImpl.kt b/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/BookRepositoryImpl.kt new file mode 100644 index 0000000..22b2b4e --- /dev/null +++ b/server/src/main/kotlin/net/keinekekse/app/server/testdb/model/BookRepositoryImpl.kt @@ -0,0 +1,31 @@ +package net.keinekekse.app.server.testdb.model + +class BookRepositoryImpl : BookRepository { + override suspend fun allBooks(): List { + TODO("Not yet implemented") + } + + override suspend fun bookById(id: Long): Book? { + TODO("Not yet implemented") + } + + override suspend fun booksBySeries(series: String): List { + TODO("Not yet implemented") + } + + override suspend fun bookByTitle(title: String): List { + TODO("Not yet implemented") + } + + override suspend fun bookByAuthor(author: String): List { + TODO("Not yet implemented") + } + + override suspend fun addTBook(book: Book) { + TODO("Not yet implemented") + } + + override suspend fun removeBook(id: Long): Boolean { + TODO("Not yet implemented") + } +} diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf index fc58843..d228f3d 100644 --- a/server/src/main/resources/application.conf +++ b/server/src/main/resources/application.conf @@ -18,18 +18,19 @@ ktor { # ApplicationKt.module() übernimmt die Konfiguration modules = [ net.keinekekse.app.ApplicationKt.module ] } - conf { - # eigene Parameter -> ktor.conf. - # - keycloakInternalUrl = "http://kec:8080/realms" - keycloakUrl = "https://kec.keinekekse.net/realms" - keycloakRealm = "app-server-realm" - keycloakClientId = "pkce-book-app" - keycloakCertUrl = "protocol/openid-connect/certs" - # DB-Params werden vom Container als env vars gesetzt - dbHost = ${DB_HOST} - dbPort = ${DB_PORT} - dbUser = ${DB_USER} - dbPassword = ${DB_PASSWORD} - } +} +keycloak { + internalUrl = "http://kec:8080/realms" + url = "https://kec.keinekekse.net/realms" + realm = "app-server-realm" + clientId = "pkce-book-app" + certUrl = "protocol/openid-connect/certs" +} +postgres { + # DB-Params werden vom Container als env vars gesetzt + host = ${DB_HOST} + port = ${DB_PORT} + database = ${DB_DATABASE} + user = ${DB_USER} + password = ${DB_PASSWORD} }