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 'org.jetbrains.kotlin.plugin.serialization'
id 'com.mikepenz.aboutlibraries.plugin' id 'com.mikepenz.aboutlibraries.plugin'
id 'com.google.dagger.hilt.android' id 'com.google.dagger.hilt.android'
id 'kotlin-kapt' id 'kotlin-kapt'
} }
@ -89,6 +90,7 @@ dependencies {
// Hilt - Dependency Injection // Hilt - Dependency Injection
implementation "com.google.dagger:hilt-android:$hilt_version" 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" kapt "com.google.dagger:hilt-compiler:$hilt_version"
// Coil - Image Loading // 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.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.composable
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -1,9 +1,5 @@
package chat.revolt.api 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.routes.user.fetchSelf
import chat.revolt.api.schemas.CompleteUser import chat.revolt.api.schemas.CompleteUser
import io.ktor.client.* import io.ktor.client.*
@ -21,8 +17,6 @@ const val REVOLT_FILES = "https://autumn.revolt.chat"
private const val BACKEND_IS_STABLE = false private const val BACKEND_IS_STABLE = false
val Context.revoltKVStorage: DataStore<Preferences> by preferencesDataStore(name = "revolt_kv")
val RevoltJson = Json { ignoreUnknownKeys = true } val RevoltJson = Json { ignoreUnknownKeys = true }
val RevoltHttp = HttpClient(OkHttp) { val RevoltHttp = HttpClient(OkHttp) {
@ -51,6 +45,7 @@ val RevoltHttp = HttpClient(OkHttp) {
} }
} }
object RevoltAPI { object RevoltAPI {
const val TOKEN_HEADER_NAME = "x-session-token" 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.api.REVOLT_FILES import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.components.generic.RemoteImage 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 @Composable
fun HomeScreen(navController: NavController) { fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) {
val user = RevoltAPI.userCache[RevoltAPI.selfId] val user = RevoltAPI.userCache[RevoltAPI.selfId]
Column() { Column() {
@ -61,7 +79,7 @@ fun HomeScreen(navController: NavController) {
} }
Button( Button(
onClick = { onClick = {
RevoltAPI.logout() viewModel.logout()
navController.navigate("login/greeting") { navController.navigate("login/greeting") {
popUpTo("chat/home") { popUpTo("chat/home") {
inclusive = true 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.components.generic.RemoteImage import chat.revolt.components.generic.RemoteImage
import chat.revolt.components.generic.drawableResource import chat.revolt.components.generic.drawableResource
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch 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) private var _skipLogin by mutableStateOf(false)
val skipLogin: Boolean val skipLogin: Boolean
get() = _skipLogin get() = _skipLogin
@ -47,17 +53,24 @@ class GreeterViewModel() : ViewModel() {
init { init {
viewModelScope.launch { viewModelScope.launch {
val token = kvStorage.get("sessionToken")
if (token != null) {
RevoltAPI.setSessionHeader(token)
}
RevoltAPI.initialize() RevoltAPI.initialize()
if (RevoltAPI.isLoggedIn()) { if (RevoltAPI.isLoggedIn()) {
_skipLogin = true _skipLogin = true
} }
setFinishedLoading(true) setFinishedLoading(true)
} }
} }
} }
@Composable @Composable
fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = viewModel()) { fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = hiltViewModel()) {
if (viewModel.skipLogin) { if (viewModel.skipLogin) {
navController.navigate("chat/home") { navController.navigate("chat/home") {
popUpTo("login/greeting") { 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.REVOLT_SUPPORT 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.AnyLink
import chat.revolt.components.generic.FormTextField import chat.revolt.components.generic.FormTextField
import chat.revolt.components.generic.Weblink import chat.revolt.components.generic.Weblink
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch 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("") private var _email by mutableStateOf("")
val email: String val email: String
get() = _email get() = _email
@ -68,7 +74,10 @@ class LoginViewModel() : ViewModel() {
"Login", "Login",
"No MFA required. Login is complete! We have a session token: ${response.firstUserHints!!.token}" "No MFA required. Login is complete! We have a session token: ${response.firstUserHints!!.token}"
) )
fetchSelfWithNewToken(response.firstUserHints.token) fetchSelfWithNewToken(response.firstUserHints.token)
kvStorage.set("sessionToken", response.firstUserHints.token)
_navigateTo = "home" _navigateTo = "home"
} }
} }
@ -91,7 +100,7 @@ class LoginViewModel() : ViewModel() {
@Composable @Composable
fun LoginScreen( fun LoginScreen(
navController: NavController, navController: NavController,
viewModel: LoginViewModel = viewModel() viewModel: LoginViewModel = hiltViewModel()
) { ) {
if (viewModel.navigateTo == "mfa") { if (viewModel.navigateTo == "mfa") {
navController.navigate( 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.routes.account.MfaResponseRecoveryCode 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.api.routes.user.fetchSelfWithNewToken
import chat.revolt.components.generic.CollapsibleCard import chat.revolt.components.generic.CollapsibleCard
import chat.revolt.components.generic.FormTextField import chat.revolt.components.generic.FormTextField
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch 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("") private var _totpCode by mutableStateOf("")
val totpCode: String val totpCode: String
get() = _totpCode get() = _totpCode
@ -72,8 +78,10 @@ class MfaScreenViewModel : ViewModel() {
"MFA", "MFA",
"Successfully authorized TOTP. Token: ${response.firstUserHints!!.token}" "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 _navigateToHome = true
} }
} }
@ -91,8 +99,10 @@ class MfaScreenViewModel : ViewModel() {
"MFA", "MFA",
"Successfully authorized recovery code. Token: ${response.firstUserHints!!.token}" "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 _navigateToHome = true
} }
} }
@ -104,7 +114,7 @@ fun MfaScreen(
navController: NavController, navController: NavController,
allowedAuthTypesCommaSep: String, allowedAuthTypesCommaSep: String,
mfaTicket: String, mfaTicket: String,
viewModel: MfaScreenViewModel = viewModel() viewModel: MfaScreenViewModel = hiltViewModel()
) { ) {
val allowedAuthTypes = allowedAuthTypesCommaSep.split(",") val allowedAuthTypes = allowedAuthTypesCommaSep.split(",")