Implemented session validation and token refreshing
Added a `SessionState` sealed class to track the connection status and integrated it into the `DashboardScreen`. The dashboard now triggers a session validation on launch, showing a loading indicator during the check. The `MainViewModel` has been updated with a `validateSession` function that: - Pings the server using the stored access token. - Attempts to refresh the token if a 401 Unauthorized response is received. - Updates the `TokenStorage` with new tokens upon successful refresh. - Redirects to logout if both tokens are invalid. Additionally, the `APIService` was expanded with `ping` and `refresh` endpoints, and the `AppDatabase` version was incremented.
This commit is contained in:
@@ -24,6 +24,7 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
@@ -43,6 +44,7 @@ import androidx.compose.ui.unit.dp
|
||||
import de.miaurizius.shap_planner.TokenStorage
|
||||
import de.miaurizius.shap_planner.UserPreferences
|
||||
import de.miaurizius.shap_planner.entities.Account
|
||||
import de.miaurizius.shap_planner.network.SessionState
|
||||
import de.miaurizius.shap_planner.room.AppDatabase
|
||||
import de.miaurizius.shap_planner.ui.theme.ShapPlannerTheme
|
||||
import de.miaurizius.shap_planner.viewmodels.LoginViewModel
|
||||
@@ -97,7 +99,10 @@ class MainActivity : ComponentActivity() {
|
||||
DashboardScreen(
|
||||
account = selectedAccount,
|
||||
onBack = { mainViewModel.logoutFromAccount() },
|
||||
onDelete = { mainViewModel.deleteAccount(selectedAccount) }
|
||||
onDelete = { mainViewModel.deleteAccount(selectedAccount) },
|
||||
sessionState = mainViewModel.sessionState,
|
||||
onValidate = { mainViewModel.validateSession(selectedAccount) },
|
||||
onSessionInvalid = { mainViewModel.logoutFromAccount() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -208,12 +213,26 @@ fun LoginScreen(onLogin: (String, String, String) -> Unit, onBack: (() -> Unit)?
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DashboardScreen(account: Account, onBack: () -> Unit, onDelete: () -> Unit) {
|
||||
fun DashboardScreen(
|
||||
account: Account,
|
||||
onBack: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
sessionState: SessionState,
|
||||
onValidate: () -> Unit,
|
||||
onSessionInvalid: () -> Unit) {
|
||||
|
||||
LaunchedEffect(Unit) { onValidate() }
|
||||
|
||||
when (sessionState) {
|
||||
SessionState.Loading -> {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
SessionState.Valid -> {
|
||||
BackHandler {
|
||||
onBack()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -253,3 +272,14 @@ fun DashboardScreen(account: Account, onBack: () -> Unit, onDelete: () -> Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
SessionState.Invalid -> {
|
||||
LaunchedEffect(Unit) {
|
||||
onSessionInvalid()
|
||||
}
|
||||
}
|
||||
is SessionState.Error -> {
|
||||
Text("Server error")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,16 +2,25 @@ package de.miaurizius.shap_planner.network
|
||||
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
data class LoginRequest(val username: String, val password: String)
|
||||
data class LoginUser(val id: String, val username: String, val role: String, val avatarUrl: String?)
|
||||
data class LoginResponse(val access_token: String, val refresh_token: String, val user: LoginUser, val wgName: String)
|
||||
|
||||
data class RefreshRequest(val refreshToken: String)
|
||||
data class RefreshResponse(val accessToken: String, val refreshToken: String)
|
||||
|
||||
interface APIService {
|
||||
@POST("api/login")
|
||||
suspend fun login(@Body req: LoginRequest): Response<LoginResponse>
|
||||
|
||||
@POST("api/refresh")
|
||||
suspend fun refresh(@Body req: Map<String, String>): Response<Map<String, String>>
|
||||
suspend fun refresh(@Body req: RefreshRequest): Response<RefreshResponse>
|
||||
|
||||
@GET("api/ping")
|
||||
suspend fun ping(@Header("Authorization") token: String): Response<Map<String, String>>
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.miaurizius.shap_planner.network
|
||||
|
||||
sealed class SessionState {
|
||||
object Loading : SessionState()
|
||||
object Valid : SessionState()
|
||||
object Invalid : SessionState()
|
||||
data class Error(val message: String) : SessionState()
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import androidx.room.RoomDatabase
|
||||
import de.miaurizius.shap_planner.entities.Account
|
||||
import de.miaurizius.shap_planner.entities.AccountDao
|
||||
|
||||
@Database(entities = [Account::class], version = 2)
|
||||
@Database(entities = [Account::class], version = 3)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun accountDao(): AccountDao
|
||||
companion object {
|
||||
|
||||
@@ -8,6 +8,9 @@ import androidx.lifecycle.viewModelScope
|
||||
import de.miaurizius.shap_planner.TokenStorage
|
||||
import de.miaurizius.shap_planner.entities.Account
|
||||
import de.miaurizius.shap_planner.entities.AccountDao
|
||||
import de.miaurizius.shap_planner.network.RefreshRequest
|
||||
import de.miaurizius.shap_planner.network.RetrofitProvider
|
||||
import de.miaurizius.shap_planner.network.SessionState
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -19,6 +22,54 @@ class MainViewModel(private val accountDao: AccountDao, private val tokenStorage
|
||||
val accounts: StateFlow<List<Account>> = accountDao.getAllAccounts()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
var sessionState by mutableStateOf<SessionState>(SessionState.Loading)
|
||||
private set
|
||||
|
||||
fun validateSession(account: Account) {
|
||||
viewModelScope.launch {
|
||||
sessionState = SessionState.Loading
|
||||
val api = RetrofitProvider.create(account.serverUrl)
|
||||
|
||||
val accessToken = tokenStorage.getAccess(account.id.toString())
|
||||
val refreshToken = tokenStorage.getRefresh(account.id.toString())
|
||||
|
||||
if(accessToken == null || refreshToken == null) {
|
||||
sessionState = SessionState.Invalid
|
||||
return@launch
|
||||
}
|
||||
|
||||
println("Testing with AT $accessToken")
|
||||
val pingResponse = api.ping("Bearer $accessToken")
|
||||
|
||||
if(pingResponse.isSuccessful) {
|
||||
sessionState = SessionState.Valid
|
||||
return@launch
|
||||
}
|
||||
|
||||
if(pingResponse.code() == 401) {
|
||||
println("Testing with RT $refreshToken")
|
||||
val refreshResponse = api.refresh(RefreshRequest(refreshToken))
|
||||
|
||||
if(refreshResponse.isSuccessful) {
|
||||
val newTokens = refreshResponse.body()!!
|
||||
|
||||
tokenStorage.saveTokens(
|
||||
account.id.toString(),
|
||||
newTokens.accessToken,
|
||||
newTokens.accessToken
|
||||
)
|
||||
|
||||
sessionState = SessionState.Valid
|
||||
return@launch
|
||||
} else {
|
||||
sessionState = SessionState.Invalid
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
sessionState = SessionState.Error("Server error")
|
||||
}
|
||||
}
|
||||
|
||||
fun addAccount(account: Account) {
|
||||
viewModelScope.launch {
|
||||
accountDao.insertAccount(account)
|
||||
|
||||
Reference in New Issue
Block a user