#1 Setings ist erledigt
This commit is contained in:
1 parent
632d7c34ef
commit
698490a6f8
8 files changed
+268
-164
No files matched your search
@@ -1,3 +1,3 @@
|
|||||||
#auto generated
|
#auto generated
|
||||||
#Thu Feb 05 01:16:28 CET 2026
|
#Fri Feb 06 16:36:58 CET 2026
|
||||||
build.number=190
|
build.number=212
|
||||||
@@ -4,13 +4,13 @@ import net.keinekekse.version.Version
|
|||||||
|
|
||||||
object VERSION: Version {
|
object VERSION: Version {
|
||||||
override val version = "1.0.0"
|
override val version = "1.0.0"
|
||||||
override val fullVersion = "1.0.0+build.190"
|
override val fullVersion = "1.0.0+build.212"
|
||||||
override val major = 1
|
override val major = 1
|
||||||
override val minor = 0
|
override val minor = 0
|
||||||
override val patch = 0
|
override val patch = 0
|
||||||
override val preRelease = ""
|
override val preRelease = ""
|
||||||
override val buildMetadata = "build.190"
|
override val buildMetadata = "build.212"
|
||||||
override val buildDate = "2026-02-05T00:16:28.206204Z"
|
override val buildDate = "2026-02-06T15:36:58.060625Z"
|
||||||
override val displayName = "Books App"
|
override val displayName = "Books App"
|
||||||
override val copyright = "Copyright (c) 2026 Hartmut Bauer"
|
override val copyright = "Copyright (c) 2026 Hartmut Bauer"
|
||||||
override val name = "books"
|
override val name = "books"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.keinekekse.app.auth
|
package net.keinekekse.app.auth
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import net.keinekekse.app.config.ConfigProperties
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
@@ -18,13 +19,13 @@ sealed interface AuthResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
fun createKeycloakConfig(): KeycloakConfig {
|
fun createKeycloakConfig(config: ConfigProperties): KeycloakConfig {
|
||||||
|
|
||||||
// TODO muss aus config geholt werden (<--> DI??)
|
// TODO muss aus config geholt werden (<--> DI??)
|
||||||
|
|
||||||
val keycloakServerHostname = "kec.keinekekse.net"
|
val keycloakServerHostname = config["keycloakServer"] ?: ""
|
||||||
val keycloakRealm = "app-server-realm"
|
val keycloakRealm = config["keycloakRealm"] ?: ""
|
||||||
val keycloakClient = "pkce-book-app"
|
val keycloakClient = config["keycloakClient"] ?: ""
|
||||||
|
|
||||||
return KeycloakConfig(keycloakServerHostname, keycloakRealm, keycloakClient)
|
return KeycloakConfig(keycloakServerHostname, keycloakRealm, keycloakClient)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.keinekekse.app.auth
|
package net.keinekekse.app.auth
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
@@ -18,36 +19,69 @@ import io.ktor.utils.io.core.Closeable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import net.keinekekse.app.service.TokenService
|
import net.keinekekse.app.service.TokenService
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
import java.util.*
|
||||||
import kotlin.use
|
import kotlin.use
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class Authenticator(
|
class Authenticator(
|
||||||
// @Property("kc_server") keycloakServerHostname: String,
|
|
||||||
// @Property("kc_realm") keycloakRealm: String,
|
|
||||||
// @Property("kc_client") keycloakClient: String,
|
|
||||||
val tokenService: TokenService,
|
val tokenService: TokenService,
|
||||||
private val keycloakConfig: KeycloakConfig,
|
private val keycloakConfig: KeycloakConfig,
|
||||||
) : Closeable {
|
|
||||||
|
) : Closeable {
|
||||||
private val httpClient = HttpClient {
|
private val httpClient = HttpClient {
|
||||||
install(ContentNegotiation) { json() }
|
install(ContentNegotiation) {
|
||||||
|
json(
|
||||||
|
Json { ignoreUnknownKeys = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
install(HttpTimeout) {
|
install(HttpTimeout) {
|
||||||
requestTimeoutMillis = 10000
|
requestTimeoutMillis = 10000
|
||||||
connectTimeoutMillis = 5000
|
connectTimeoutMillis = 5000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private val keycloakConfig =
|
|
||||||
// KeycloakConfig(keycloakServerHostname, keycloakRealm, keycloakClient)
|
|
||||||
|
|
||||||
override fun close() = httpClient.close()
|
override fun close() = httpClient.close()
|
||||||
|
|
||||||
suspend fun authenticate(): AuthResult {
|
suspend fun authenticate(): AuthResult {
|
||||||
val config = AuthRun(keycloakConfig)
|
// check if necessary
|
||||||
|
val tokens = tokenService.getTokens()
|
||||||
|
if (tokens != null) {
|
||||||
|
val access = JWT.decode(tokens.accessToken)
|
||||||
|
val refresh = JWT.decode(tokens.refreshToken)
|
||||||
|
val now = Date()
|
||||||
|
println("Now: $now")
|
||||||
|
println("Token expires at: ${access.expiresAt}")
|
||||||
|
println("Token is expired: ${access.expiresAt.before(Date())}")
|
||||||
|
println("Refresh token expires at: ${refresh.expiresAt}")
|
||||||
|
println("Refresh token is expired: ${refresh.expiresAt.before(Date())}")
|
||||||
|
if (access.expiresAt.before(now)) {
|
||||||
|
// access_token ungültig
|
||||||
|
if (refresh.expiresAt.before(now)) {
|
||||||
|
// refresh_token ungültig
|
||||||
|
return doAuthentication()
|
||||||
|
} else {
|
||||||
|
val result = requestRefreshToken(tokens)
|
||||||
|
if (result != null) {
|
||||||
|
return AuthResult.Success()
|
||||||
|
} else {
|
||||||
|
return doAuthentication()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return AuthResult.Success()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return doAuthentication()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doAuthentication(): AuthResult {
|
||||||
|
val config = AuthRun(keycloakConfig)
|
||||||
if (!testServerConnection(config)) {
|
if (!testServerConnection(config)) {
|
||||||
return AuthResult.Error("ERR001", "Der Anmeldeserver ist nicht erreichbar.", "")
|
return AuthResult.Error("ERR001", "Der Anmeldeserver ist nicht erreichbar.", "")
|
||||||
}
|
}
|
||||||
@@ -58,7 +92,7 @@ class Authenticator(
|
|||||||
config.buildRedirectUri(port)
|
config.buildRedirectUri(port)
|
||||||
if (!waitForServer(port)) {
|
if (!waitForServer(port)) {
|
||||||
return AuthResult.Fatal(
|
return AuthResult.Fatal(
|
||||||
"ERR008",
|
"ERR009",
|
||||||
"Schwerer Fehler",
|
"Schwerer Fehler",
|
||||||
"Der interne Webserver konnte nicht gestartet werden. Keine Anmeldung möglich."
|
"Der interne Webserver konnte nicht gestartet werden. Keine Anmeldung möglich."
|
||||||
)
|
)
|
||||||
@@ -79,6 +113,14 @@ class Authenticator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun reAuthenticate(): BearerTokens? {
|
||||||
|
val authResult = authenticate()
|
||||||
|
if (authResult is AuthResult.Success) {
|
||||||
|
return tokenService.getTokens()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun requestRefreshToken(tokens: BearerTokens?): BearerTokens? {
|
suspend fun requestRefreshToken(tokens: BearerTokens?): BearerTokens? {
|
||||||
val config = AuthRun(keycloakConfig)
|
val config = AuthRun(keycloakConfig)
|
||||||
|
|
||||||
@@ -103,7 +145,33 @@ class Authenticator(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
tokenService.setTokens(null)
|
tokenService.setTokens(null)
|
||||||
return null
|
println("Error refreshing tokens: ")
|
||||||
|
var error = ""
|
||||||
|
var errorDetails = ""
|
||||||
|
when (val err = result.first) {
|
||||||
|
is AuthResult.Fatal -> {
|
||||||
|
println(err.code)
|
||||||
|
error = err.message ?: ""
|
||||||
|
errorDetails = err.details ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
is AuthResult.Error -> {
|
||||||
|
println(err.code)
|
||||||
|
error = err.message ?: ""
|
||||||
|
errorDetails = err.details ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
is AuthResult.Success -> println("Success ???")
|
||||||
|
}
|
||||||
|
println("Status: ${response.status}")
|
||||||
|
println("Error: $error")
|
||||||
|
println("Description: $errorDetails")
|
||||||
|
if (error == "invalid_grant") {
|
||||||
|
// session expired || token is invalid
|
||||||
|
return reAuthenticate()
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,10 +384,10 @@ class Authenticator(
|
|||||||
try {
|
try {
|
||||||
val errorResponse = response.body<TokenErrorResponse>()
|
val errorResponse = response.body<TokenErrorResponse>()
|
||||||
return Pair(
|
return Pair(
|
||||||
AuthResult.Error(
|
AuthResult.Fatal(
|
||||||
"ERR006",
|
"ERR010",
|
||||||
"HTTP-Fehler ${response.status.value} beim Abholen der Zugriffstoken",
|
errorResponse.error,
|
||||||
"${errorResponse.error}: ${errorResponse.errorDescription ?: ""}"
|
"${errorResponse.errorDescription} (${response.status})"
|
||||||
), null
|
), null
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -327,7 +395,7 @@ class Authenticator(
|
|||||||
val errorResponse = response.bodyAsText()
|
val errorResponse = response.bodyAsText()
|
||||||
return Pair(
|
return Pair(
|
||||||
AuthResult.Error(
|
AuthResult.Error(
|
||||||
"ERR006",
|
"ERR007",
|
||||||
"HTTP-Fehler ${response.status.value} beim Abholen der Zugriffstoken",
|
"HTTP-Fehler ${response.status.value} beim Abholen der Zugriffstoken",
|
||||||
errorResponse
|
errorResponse
|
||||||
), null
|
), null
|
||||||
@@ -340,7 +408,7 @@ class Authenticator(
|
|||||||
// Fehler beim JSON-Konvertieren
|
// Fehler beim JSON-Konvertieren
|
||||||
return Pair(
|
return Pair(
|
||||||
AuthResult.Fatal(
|
AuthResult.Fatal(
|
||||||
"ERR007",
|
"ERR008",
|
||||||
"Schwerer Fehler",
|
"Schwerer Fehler",
|
||||||
"Ungültige Daten beim Abholen der Zugriffstoken. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
|
"Ungültige Daten beim Abholen der Zugriffstoken. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
|
||||||
), null
|
), null
|
||||||
|
|||||||
@@ -1,28 +1,9 @@
|
|||||||
package net.keinekekse.app
|
package net.keinekekse.app
|
||||||
|
|
||||||
//import org.koin.plugin.module.dsl.startKoin
|
//import org.koin.plugin.module.dsl.startKoin
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.keinekekse.app.auth.AuthResult
|
|
||||||
import net.keinekekse.app.auth.Authenticator
|
|
||||||
import net.keinekekse.app.books.di.KoinApp
|
import net.keinekekse.app.books.di.KoinApp
|
||||||
import net.keinekekse.app.config.ConfigProperties
|
|
||||||
import net.keinekekse.app.config.Settings
|
|
||||||
import net.keinekekse.app.ui.ProgressIndicator
|
|
||||||
import org.koin.compose.koinInject
|
|
||||||
import org.koin.plugin.module.dsl.startKoin
|
import org.koin.plugin.module.dsl.startKoin
|
||||||
|
|
||||||
//@PropertyValue("kc_server")
|
//@PropertyValue("kc_server")
|
||||||
@@ -38,126 +19,118 @@ fun main() = application {
|
|||||||
|
|
||||||
startKoin<KoinApp>()
|
startKoin<KoinApp>()
|
||||||
|
|
||||||
val config: ConfigProperties = koinInject()
|
// val config: ConfigProperties = koinInject()
|
||||||
val settings: Settings = koinInject()
|
// val settings: Settings = koinInject()
|
||||||
|
|
||||||
// val settings: Settings = PreferencesSettings(delegate)
|
// val settings: Settings = PreferencesSettings(delegate)
|
||||||
//
|
//
|
||||||
// settings.put("kc_server", "kec.keinekekse.net")
|
// settings.putInt("intValue", 42)
|
||||||
println("Properties: ${config["keycloakServer"]}")
|
// val sett = settings.export()
|
||||||
println("Settings: ${settings.get("kc_server", "default")}")
|
// settings.putInt("intValue", 4242)
|
||||||
// println(Preferences.userRoot())
|
// println("Properties: ${config["keycloakServer"]}")
|
||||||
|
// println("Settings: ${settings.get("kc_server", "default")}")
|
||||||
// println("System properties:")
|
// println(settings.export())
|
||||||
// System.getProperties().forEach { println("${it.key}=${it.value}") }
|
// settings.import(sett)
|
||||||
// println("")
|
// println("Settings: ${settings.getInt("intValue", -1)}")
|
||||||
|
|
||||||
// keycloakServer = "kec.keinekekse.net"
|
|
||||||
// keycloakRealm = "app-server-realm"
|
|
||||||
// keycloakClient = "pkce-book-app"
|
|
||||||
|
|
||||||
// val inj = koinInject<String>(named("AppString"))
|
// val inj = koinInject<String>(named("AppString"))
|
||||||
|
|
||||||
val authResultState = remember { mutableStateOf<AuthResult?>(null) }
|
// val authResultState = remember { mutableStateOf<AuthResult?>(null) }
|
||||||
|
//
|
||||||
|
// val authenticator: Authenticator = koinInject()
|
||||||
|
|
||||||
val authenticator: Authenticator = koinInject()
|
// LaunchedEffect(Unit) {
|
||||||
|
// authResultState.value = withContext(Dispatchers.IO) {
|
||||||
LaunchedEffect(Unit) {
|
// // TODO - (~) Verständnis für App-Aufbau und DI
|
||||||
authResultState.value = withContext(Dispatchers.IO) {
|
// // - (x) das object muss global angelegt werden -> DI singleton
|
||||||
// TODO - (~) Verständnis für App-Aufbau und DI
|
// // - (x) globale Speicherung der Tokens
|
||||||
// - (x) das object muss global angelegt werden -> DI singleton
|
// // - Tokens persistieren? Multiplatform! (evtl. library)
|
||||||
// - (x) globale Speicherung der Tokens
|
// // - (x) globaler httpClient mit Konfig für Tokens -> DI
|
||||||
// - Tokens persistieren? Multiplatform! (evtl. library)
|
// // - (x) Error-Handling: 401 -> refresh -> 401 -> reauth [implizit]
|
||||||
// - (x) globaler httpClient mit Konfig für Tokens -> DI
|
//
|
||||||
// - (x) Error-Handling: 401 -> refresh -> 401 -> reauth [implizit]
|
// val result = authenticator.authenticate()
|
||||||
// Authenticator(
|
//
|
||||||
// "kec.keinekekse.net",
|
// when (result) {
|
||||||
// "app-server-realm",
|
// is AuthResult.Success -> {
|
||||||
// "pkce-book-app",
|
//// println("Access Token: ${result.tokens?.accessToken}")
|
||||||
// ).use {
|
//// println("Refresh Token: ${result.tokens?.refreshToken}")
|
||||||
|
//
|
||||||
val result = authenticator.authenticate()
|
//// delay(3000)
|
||||||
|
// /*
|
||||||
when (result) {
|
// // klappt, gibt aber nicht mehr her, als ohnehin im Token steht
|
||||||
is AuthResult.Success -> {
|
// it.requestUserInfo(result.tokens!!.accessToken)
|
||||||
// println("Access Token: ${result.tokens?.accessToken}")
|
// */
|
||||||
// println("Refresh Token: ${result.tokens?.refreshToken}")
|
// /*
|
||||||
|
// // funktioniert -> jetzt fehlt die DI-Mimik für die compose app
|
||||||
// delay(3000)
|
// when (val res = it.requestRefreshToken(result.tokens)) {
|
||||||
/*
|
// is AuthResult.Success -> {
|
||||||
// klappt, gibt aber nicht mehr her, als ohnehin im Token steht
|
// println("Access Token: ${res.tokens?.accessToken}")
|
||||||
it.requestUserInfo(result.tokens!!.accessToken)
|
// println("Refresh Token: ${res.tokens?.refreshToken}")
|
||||||
*/
|
// }
|
||||||
/*
|
//
|
||||||
// funktioniert -> jetzt fehlt die DI-Mimik für die compose app
|
// is AuthResult.Error -> {
|
||||||
when (val res = it.requestRefreshToken(result.tokens)) {
|
// println("Refresh Token fehlgeschlagen ${res.code}: ${res.details ?: ""}")
|
||||||
is AuthResult.Success -> {
|
// }
|
||||||
println("Access Token: ${res.tokens?.accessToken}")
|
//
|
||||||
println("Refresh Token: ${res.tokens?.refreshToken}")
|
// is AuthResult.Fatal -> {
|
||||||
}
|
// println("Refresh Token fehlgeschlagen ${res.code}: ${res.details ?: ""}")
|
||||||
|
//
|
||||||
is AuthResult.Error -> {
|
// }
|
||||||
println("Refresh Token fehlgeschlagen ${res.code}: ${res.details ?: ""}")
|
// }
|
||||||
}
|
// */
|
||||||
|
// }
|
||||||
is AuthResult.Fatal -> {
|
//
|
||||||
println("Refresh Token fehlgeschlagen ${res.code}: ${res.details ?: ""}")
|
// else -> {}
|
||||||
|
// }
|
||||||
}
|
//
|
||||||
}
|
// result
|
||||||
*/
|
// // }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Window(
|
Window(
|
||||||
onCloseRequest = ::exitApplication,
|
onCloseRequest = ::exitApplication,
|
||||||
title = "app",
|
title = "app",
|
||||||
) {
|
) {
|
||||||
when (val result = authResultState.value) {
|
App()
|
||||||
null -> {
|
// when (val result = authResultState.value) {
|
||||||
Column(
|
// null -> {
|
||||||
modifier = Modifier.fillMaxSize(),
|
// Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
// modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.Center
|
// horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
// verticalArrangement = Arrangement.Center
|
||||||
Text("Warten auf Anmeldung...")
|
// ) {
|
||||||
|
// Text("Warten auf Anmeldung...")
|
||||||
// Icon(
|
//
|
||||||
// imageVector = Icons.Default.HourglassTop,
|
//// Icon(
|
||||||
// contentDescription = "Home",
|
//// imageVector = Icons.Default.HourglassTop,
|
||||||
// modifier = Modifier.padding(top = 16.dp).size(32.dp)
|
//// contentDescription = "Home",
|
||||||
// )
|
//// modifier = Modifier.padding(top = 16.dp).size(32.dp)
|
||||||
ProgressIndicator()
|
//// )
|
||||||
}
|
// ProgressIndicator()
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
is AuthResult.Error -> {
|
//
|
||||||
// TODO show an error UI (and maybe a retry button)
|
// is AuthResult.Error -> {
|
||||||
Text("Fehler bei der Anmeldung: ${result.message} (${result.code})")
|
// // TODO show an error UI (and maybe a retry button)
|
||||||
Text("Fehlerbeschreibung: ${result.details ?: ""}")
|
// Text("Fehler bei der Anmeldung: ${result.message} (${result.code})")
|
||||||
|
// Text("Fehlerbeschreibung: ${result.details ?: ""}")
|
||||||
|
//// App()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is AuthResult.Fatal -> {
|
||||||
|
// Text("Fataler Fehler bei der Anmeldung: ${result.message} (${result.code})")
|
||||||
|
// Text("Fehlerbeschreibung: ${result.details ?: ""}")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is AuthResult.Success -> {
|
||||||
|
// val test = koinInject<TestAuth>()
|
||||||
|
// CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
// println("requesting '/'")
|
||||||
|
// test.request("/")
|
||||||
|
// }
|
||||||
// App()
|
// App()
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
is AuthResult.Fatal -> {
|
|
||||||
Text("Fataler Fehler bei der Anmeldung: ${result.message} (${result.code})")
|
|
||||||
Text("Fehlerbeschreibung: ${result.details ?: ""}")
|
|
||||||
}
|
|
||||||
|
|
||||||
is AuthResult.Success -> {
|
|
||||||
val test = koinInject<TestAuth>()
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
println("requesting '/'")
|
|
||||||
test.request("/")
|
|
||||||
}
|
|
||||||
App()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,22 +1,84 @@
|
|||||||
package net.keinekekse.app.service
|
package net.keinekekse.app.service
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.plugins.auth.*
|
import io.ktor.client.plugins.auth.*
|
||||||
import io.ktor.client.plugins.auth.providers.*
|
import io.ktor.client.plugins.auth.providers.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import net.keinekekse.app.auth.Authenticator
|
import net.keinekekse.app.auth.Authenticator
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Single
|
@Single
|
||||||
fun createHttpClient(authenticator: Authenticator) = HttpClient {
|
fun createHttpClient(authenticator: Authenticator): HttpClient {
|
||||||
install(Auth) {
|
val client = HttpClient {
|
||||||
bearer {
|
install(Auth) {
|
||||||
loadTokens {
|
bearer {
|
||||||
authenticator.tokenService.getTokens()
|
loadTokens {
|
||||||
}
|
println("HTTPClient: loadTokens")
|
||||||
refreshTokens {
|
// tokenService.getTokens()
|
||||||
authenticator.requestRefreshToken(oldTokens)
|
authenticator.tokenService.getTokens()
|
||||||
authenticator.tokenService.getTokens()
|
}
|
||||||
|
refreshTokens {
|
||||||
|
println("HTTPClient: refreshTokens")
|
||||||
|
// val tokens = authenticator.tokenService.getTokens()
|
||||||
|
if (oldTokens != null) {
|
||||||
|
val access = JWT.decode(oldTokens?.accessToken)
|
||||||
|
val refresh = JWT.decode(oldTokens?.refreshToken)
|
||||||
|
val now = Date()
|
||||||
|
println("Now: $now")
|
||||||
|
println("Token expires at: ${access.expiresAt}")
|
||||||
|
println("Token is expired: ${access.expiresAt.before(Date())}")
|
||||||
|
println("Refresh token expires at: ${refresh.expiresAt}")
|
||||||
|
println("Refresh token is expired: ${refresh.expiresAt.before(Date())}")
|
||||||
|
|
||||||
|
if (refresh.expiresAt.before(now)) {
|
||||||
|
println("refresh token expired - HOW TO authenticate")
|
||||||
|
authenticator.reAuthenticate()
|
||||||
|
} else {
|
||||||
|
println("refresh token valid - request new tokens")
|
||||||
|
authenticator.requestRefreshToken(oldTokens)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("no tokens - authenticate")
|
||||||
|
authenticator.reAuthenticate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json {
|
||||||
|
prettyPrint = true
|
||||||
|
allowTrailingComma = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
install(HttpTimeout) {
|
||||||
|
requestTimeoutMillis = 10000
|
||||||
|
connectTimeoutMillis = 5000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.plugin(HttpSend).intercept { request ->
|
||||||
|
println("HTTPClient: intercept ${request.url}")
|
||||||
|
val originalCall = execute(request)
|
||||||
|
println("HTTPClient: response status ${originalCall.response.status}")
|
||||||
|
originalCall
|
||||||
|
// val originalCall = execute(request)
|
||||||
|
// if (originalCall.response.status.value !in 100..399) {
|
||||||
|
// execute(request)
|
||||||
|
// } else {
|
||||||
|
// originalCall
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,8 @@ import org.koin.core.annotation.Single
|
|||||||
class TokenService {
|
class TokenService {
|
||||||
private var tokens: BearerTokens? = null
|
private var tokens: BearerTokens? = null
|
||||||
|
|
||||||
// TODO persist tokens
|
|
||||||
|
|
||||||
fun getTokens(): BearerTokens? = tokens
|
fun getTokens(): BearerTokens? = tokens
|
||||||
|
|
||||||
fun setTokens(tokens: BearerTokens? = null) {
|
fun setTokens(tokens: BearerTokens? = null) {
|
||||||
this.tokens = tokens
|
this.tokens = tokens
|
||||||
println("set tokens")
|
println("set tokens")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.composeCompiler) // sonst fkt. Composable-Import nicht
|
alias(libs.plugins.composeCompiler) // sonst fkt. Composable-Import nicht
|
||||||
|
alias(libs.plugins.kotlinSerialization)
|
||||||
alias(libs.plugins.koin.compiler)
|
alias(libs.plugins.koin.compiler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user