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:
parent
8c90dbfd3e
commit
aca7817526
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(",")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue