diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/NetworkDetailView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/NetworkDetailView.kt index 803103e7..988d306d 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/NetworkDetailView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/NetworkDetailView.kt @@ -13,7 +13,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.CopyAll import androidx.compose.material.icons.outlined.OpenInFull -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -200,6 +199,7 @@ private fun Request( DetailHeadersView( headers = state.requestHeaders, labelWidth = headersLabelWidth, + onAuthorizationClicked = { token -> onAction(NetworkAction.DisplayBearerJwt(token)) }, modifier = Modifier .fillMaxWidth() .padding(12.dp) @@ -281,6 +281,7 @@ private fun Response( DetailHeadersView( headers = response.headers, labelWidth = headersLabelWidth, + onAuthorizationClicked = { token -> onAction(NetworkAction.DisplayBearerJwt(token)) }, modifier = Modifier .fillMaxWidth() .padding(12.dp) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/components/DetailHeadersView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/components/DetailHeadersView.kt index 1e98cb00..7a0047cc 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/components/DetailHeadersView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/components/DetailHeadersView.kt @@ -1,15 +1,32 @@ package io.github.openflocon.flocondesktop.features.network.detail.view.components +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import io.github.openflocon.flocondesktop.features.network.detail.model.NetworkDetailHeaderUi import io.github.openflocon.flocondesktop.features.network.detail.model.previewNetworkDetailHeaderUi import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconButton import io.github.openflocon.library.designsystem.components.FloconHorizontalDivider import io.github.openflocon.library.designsystem.components.FloconLineDescription import org.jetbrains.compose.ui.tooling.preview.Preview @@ -18,16 +35,40 @@ import org.jetbrains.compose.ui.tooling.preview.Preview fun DetailHeadersView( headers: List, labelWidth: Dp, + onAuthorizationClicked: (value: String) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { headers.fastForEachIndexed { index, item -> - FloconLineDescription( - label = item.name, - value = item.value, - labelWidth = labelWidth, - modifier = Modifier.fillMaxWidth() - ) + val isAuthBearer = item.name.equals( + "authorization", + ignoreCase = true + ) && item.value.startsWith("Bearer ") + if (isAuthBearer) { + Row(modifier = Modifier.fillMaxWidth()) { + FloconLineDescription( + label = item.name, + value = item.value, + labelWidth = labelWidth, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(4.dp)) + FloconButton( + onClick = { + onAuthorizationClicked(item.value) + } + ) { + Text("Decode\nJWT", textAlign = TextAlign.Center, color = FloconTheme.colorPalette.surface) + } + } + } else { + FloconLineDescription( + label = item.name, + value = item.value, + labelWidth = labelWidth, + modifier = Modifier.fillMaxWidth() + ) + } if (index != headers.lastIndex) { FloconHorizontalDivider(modifier = Modifier.fillMaxWidth()) } @@ -45,6 +86,7 @@ private fun DetailHeadersViewPreview() { ), labelWidth = 100.dp, modifier = Modifier.fillMaxWidth(), + onAuthorizationClicked = {}, ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/NetworkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/NetworkViewModel.kt index c6b3aae4..2e3dbe48 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/NetworkViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/NetworkViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import io.github.openflocon.domain.common.DispatcherProvider import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdAndPackageNameUseCase import io.github.openflocon.domain.feedback.FeedbackDisplayer +import io.github.openflocon.domain.network.usecase.DecodeJwtTokenUseCase import io.github.openflocon.domain.network.usecase.ExportNetworkCallsToCsvUseCase import io.github.openflocon.domain.network.usecase.GenerateCurlCommandUseCase import io.github.openflocon.domain.network.usecase.ObserveHttpRequestsByIdUseCase @@ -53,6 +54,7 @@ class NetworkViewModel( private val sortAndFilterNetworkItemsProcessor: SortAndFilterNetworkItemsProcessor, private val observeCurrentDeviceIdAndPackageNameUseCase: ObserveCurrentDeviceIdAndPackageNameUseCase, private val exportNetworkCallsToCsv: ExportNetworkCallsToCsvUseCase, + private val decodeJwtTokenUseCase: DecodeJwtTokenUseCase, ) : ViewModel(headerDelegate) { private val contentState = MutableStateFlow( @@ -157,6 +159,7 @@ class NetworkViewModel( is NetworkAction.FilterQuery -> onFilterQuery(action) is NetworkAction.CloseJsonDetail -> onCloseJsonDetail(action) is NetworkAction.JsonDetail -> onJsonDetail(action) + is NetworkAction.DisplayBearerJwt -> displayBearerJwt(action.token) is NetworkAction.ExportCsv -> onExportCsv() is NetworkAction.HeaderAction.ClickOnSort -> headerDelegate.onClickSort( type = action.type, @@ -169,6 +172,12 @@ class NetworkViewModel( } } + private fun displayBearerJwt(token: String) { + decodeJwtTokenUseCase(token)?.let { + onJsonDetail(NetworkAction.JsonDetail(id = token, json = it)) + } + } + private fun onSelectRequest(action: NetworkAction.SelectRequest) { contentState.update { state -> state.copy( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkAction.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkAction.kt index cd2b7131..f1a51e07 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkAction.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkAction.kt @@ -15,6 +15,8 @@ sealed interface NetworkAction { data class JsonDetail(val id: String, val json: String) : NetworkAction + data class DisplayBearerJwt(val token: String) : NetworkAction + data class CreateMock(val item: NetworkItemViewState) : NetworkAction data object OpenMocks : NetworkAction diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/DI.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/DI.kt index 15651541..3ba8884b 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/DI.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/DI.kt @@ -1,5 +1,6 @@ package io.github.openflocon.domain.network +import io.github.openflocon.domain.network.usecase.DecodeJwtTokenUseCase import io.github.openflocon.domain.network.usecase.ExportNetworkCallsToCsvUseCase import io.github.openflocon.domain.network.usecase.GenerateCurlCommandUseCase import io.github.openflocon.domain.network.usecase.GetNetworkFilterUseCase @@ -34,6 +35,7 @@ internal val networkModule = module { factoryOf(::RemoveHttpRequestsBeforeUseCase) factoryOf(::RemoveHttpRequestUseCase) factoryOf(::ExportNetworkCallsToCsvUseCase) + factoryOf(::DecodeJwtTokenUseCase) // filters factoryOf(::GetNetworkFilterUseCase) factoryOf(::ObserveNetworkFilterUseCase) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/DecodeJwtTokenUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/DecodeJwtTokenUseCase.kt new file mode 100644 index 00000000..1f63b644 --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/DecodeJwtTokenUseCase.kt @@ -0,0 +1,31 @@ +package io.github.openflocon.domain.network.usecase + +import java.util.Base64 + +class DecodeJwtTokenUseCase { + + operator fun invoke(token: String) : String? { + return try { + val parts = token.split(".") + if (parts.size != 3) return null + + var payload = parts[1] + + // Corrige le padding manquant (JWT enlève souvent les "=") + val padding = 4 - (payload.length % 4) + if (padding in 1..3) { + payload += "=".repeat(padding) + } + + // Remplace les caractères URL-safe par ceux de Base64 standard + payload = payload.replace('-', '+').replace('_', '/') + + val decodedBytes = Base64.getDecoder().decode(payload) + String(decodedBytes, Charsets.UTF_8) + } catch (t: Throwable) { + t.printStackTrace() + null + } + } + +} diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconLineDescription.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconLineDescription.kt index ea5dbbea..65ca911a 100644 --- a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconLineDescription.kt +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconLineDescription.kt @@ -1,5 +1,7 @@ package io.github.openflocon.library.designsystem.components +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -8,10 +10,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.github.openflocon.library.designsystem.FloconTheme