feat: persistently save user session token

this took some dependency injection work to do, but it means we can move on to the big stuff
This commit is contained in:
Infi 2022-12-11 03:06:00 +01:00
parent 8c90dbfd3e
commit aca7817526
8 changed files with 108 additions and 21 deletions

View File

@ -4,6 +4,7 @@ plugins {
id 'org.jetbrains.kotlin.plugin.serialization'
id 'com.mikepenz.aboutlibraries.plugin'
id 'com.google.dagger.hilt.android'
id 'kotlin-kapt'
}
@ -89,6 +90,7 @@ dependencies {
// Hilt - Dependency Injection
implementation "com.google.dagger:hilt-android:$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.1.0-alpha01"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
// Coil - Image Loading

View File

@ -20,7 +20,9 @@ import chat.revolt.ui.theme.RevoltTheme
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.navigation.animation.composable
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -1,9 +1,5 @@
package chat.revolt.api
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import chat.revolt.api.routes.user.fetchSelf
import chat.revolt.api.schemas.CompleteUser
import io.ktor.client.*
@ -21,8 +17,6 @@ const val REVOLT_FILES = "https://autumn.revolt.chat"
private const val BACKEND_IS_STABLE = false
val Context.revoltKVStorage: DataStore<Preferences> by preferencesDataStore(name = "revolt_kv")
val RevoltJson = Json { ignoreUnknownKeys = true }
val RevoltHttp = HttpClient(OkHttp) {
@ -51,6 +45,7 @@ val RevoltHttp = HttpClient(OkHttp) {
}
}
object RevoltAPI {
const val TOKEN_HEADER_NAME = "x-session-token"

View File

@ -0,0 +1,38 @@
package chat.revolt.persistence
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject
import javax.inject.Singleton
val Context.revoltKVStorage: DataStore<Preferences> by preferencesDataStore(name = "revolt_kv")
@Singleton
class KVStorage @Inject constructor(
@ApplicationContext private val mContext: Context
) {
private val dataStore = mContext.revoltKVStorage
suspend fun set(key: String, value: String) {
dataStore.edit { preferences ->
preferences[stringPreferencesKey(key)] = value
}
}
suspend fun get(key: String): String? {
return dataStore.data.firstOrNull()?.get(stringPreferencesKey(key))
}
suspend fun remove(key: String) {
dataStore.edit { preferences ->
preferences.remove(stringPreferencesKey(key))
}
}
}

View File

@ -13,13 +13,31 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.navigation.NavController
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.components.generic.RemoteImage
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
@HiltViewModel
class HomeScreenViewModel @Inject constructor(
private val kvStorage: KVStorage
) : ViewModel() {
fun logout() {
runBlocking {
kvStorage.remove("sessionToken")
RevoltAPI.logout()
}
}
}
@Composable
fun HomeScreen(navController: NavController) {
fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) {
val user = RevoltAPI.userCache[RevoltAPI.selfId]
Column() {
@ -61,7 +79,7 @@ fun HomeScreen(navController: NavController) {
}
Button(
onClick = {
RevoltAPI.logout()
viewModel.logout()
navController.navigate("login/greeting") {
popUpTo("chat/home") {
inclusive = true

View File

@ -19,16 +19,22 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import chat.revolt.api.RevoltAPI
import chat.revolt.components.generic.RemoteImage
import chat.revolt.components.generic.drawableResource
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
class GreeterViewModel() : ViewModel() {
@HiltViewModel
class GreeterViewModel @Inject constructor(
private val kvStorage: KVStorage
) : ViewModel() {
private var _skipLogin by mutableStateOf(false)
val skipLogin: Boolean
get() = _skipLogin
@ -47,17 +53,24 @@ class GreeterViewModel() : ViewModel() {
init {
viewModelScope.launch {
val token = kvStorage.get("sessionToken")
if (token != null) {
RevoltAPI.setSessionHeader(token)
}
RevoltAPI.initialize()
if (RevoltAPI.isLoggedIn()) {
_skipLogin = true
}
setFinishedLoading(true)
}
}
}
@Composable
fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = viewModel()) {
fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = hiltViewModel()) {
if (viewModel.skipLogin) {
navController.navigate("chat/home") {
popUpTo("login/greeting") {

View File

@ -15,9 +15,9 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.REVOLT_SUPPORT
@ -27,9 +27,15 @@ import chat.revolt.api.routes.user.fetchSelfWithNewToken
import chat.revolt.components.generic.AnyLink
import chat.revolt.components.generic.FormTextField
import chat.revolt.components.generic.Weblink
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
class LoginViewModel() : ViewModel() {
@HiltViewModel
class LoginViewModel @Inject constructor(
private val kvStorage: KVStorage,
) : ViewModel() {
private var _email by mutableStateOf("")
val email: String
get() = _email
@ -68,7 +74,10 @@ class LoginViewModel() : ViewModel() {
"Login",
"No MFA required. Login is complete! We have a session token: ${response.firstUserHints!!.token}"
)
fetchSelfWithNewToken(response.firstUserHints.token)
kvStorage.set("sessionToken", response.firstUserHints.token)
_navigateTo = "home"
}
}
@ -91,7 +100,7 @@ class LoginViewModel() : ViewModel() {
@Composable
fun LoginScreen(
navController: NavController,
viewModel: LoginViewModel = viewModel()
viewModel: LoginViewModel = hiltViewModel()
) {
if (viewModel.navigateTo == "mfa") {
navController.navigate(

View File

@ -18,9 +18,9 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.routes.account.MfaResponseRecoveryCode
@ -30,9 +30,15 @@ import chat.revolt.api.routes.account.authenticateWithMfaTotpCode
import chat.revolt.api.routes.user.fetchSelfWithNewToken
import chat.revolt.components.generic.CollapsibleCard
import chat.revolt.components.generic.FormTextField
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
class MfaScreenViewModel : ViewModel() {
@HiltViewModel
class MfaScreenViewModel @Inject constructor(
private val kvStorage: KVStorage,
) : ViewModel() {
private var _totpCode by mutableStateOf("")
val totpCode: String
get() = _totpCode
@ -72,8 +78,10 @@ class MfaScreenViewModel : ViewModel() {
"MFA",
"Successfully authorized TOTP. Token: ${response.firstUserHints!!.token}"
)
val self = fetchSelfWithNewToken(response.firstUserHints.token)
Log.d("MFA", "Self: ${self.username}")
fetchSelfWithNewToken(response.firstUserHints.token)
kvStorage.set("sessionToken", response.firstUserHints.token)
_navigateToHome = true
}
}
@ -91,8 +99,10 @@ class MfaScreenViewModel : ViewModel() {
"MFA",
"Successfully authorized recovery code. Token: ${response.firstUserHints!!.token}"
)
val self = fetchSelfWithNewToken(response.firstUserHints.token)
Log.d("MFA", "Self: ${self.username}")
fetchSelfWithNewToken(response.firstUserHints.token)
kvStorage.set("sessionToken", response.firstUserHints.token)
_navigateToHome = true
}
}
@ -104,7 +114,7 @@ fun MfaScreen(
navController: NavController,
allowedAuthTypesCommaSep: String,
mfaTicket: String,
viewModel: MfaScreenViewModel = viewModel()
viewModel: MfaScreenViewModel = hiltViewModel()
) {
val allowedAuthTypes = allowedAuthTypesCommaSep.split(",")