feat: improvements at login
- password field is now recognised by the system keyboard - TOTP six-digit input is now a numbers-only field - document API functions related to login - basic logout in API - insecure MFA-less accounts can now log in (SAD!)
This commit is contained in:
parent
8b40f122d3
commit
dff518bde7
|
|
@ -54,7 +54,9 @@ val RevoltHttp = HttpClient(OkHttp) {
|
||||||
object RevoltAPI {
|
object RevoltAPI {
|
||||||
const val TOKEN_HEADER_NAME = "x-session-token"
|
const val TOKEN_HEADER_NAME = "x-session-token"
|
||||||
|
|
||||||
val userCache = mutableMapOf<String, CompleteUser>()
|
// discount caching solution(/-s)! LRU would be better but this is fine for now, until it's not...
|
||||||
|
val userCache =
|
||||||
|
mutableMapOf<String, CompleteUser>()
|
||||||
|
|
||||||
var selfId: String? = null
|
var selfId: String? = null
|
||||||
|
|
||||||
|
|
@ -71,9 +73,23 @@ object RevoltAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is logged in and the current user has been fetched at least once.
|
||||||
|
* Call [initialize] to fetch the current user first, else this will return false.
|
||||||
|
*/
|
||||||
fun isLoggedIn(): Boolean {
|
fun isLoggedIn(): Boolean {
|
||||||
return selfId != null
|
return selfId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the API client's state completely.
|
||||||
|
*/
|
||||||
|
fun logout() {
|
||||||
|
selfId = null
|
||||||
|
sessionToken = ""
|
||||||
|
|
||||||
|
userCache.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package chat.revolt.components.generic
|
package chat.revolt.components.generic
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
|
||||||
|
|
@ -15,13 +17,14 @@ fun FormTextField(
|
||||||
label: String,
|
label: String,
|
||||||
onChange: (it: String) -> Unit,
|
onChange: (it: String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
password: Boolean = false,
|
type: KeyboardType = KeyboardType.Text,
|
||||||
) {
|
) {
|
||||||
TextField(
|
TextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onChange,
|
onValueChange = onChange,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
visualTransformation = if (password) PasswordVisualTransformation() else VisualTransformation.None,
|
keyboardOptions = KeyboardOptions(keyboardType = type),
|
||||||
|
visualTransformation = if (type == KeyboardType.Password) PasswordVisualTransformation() else VisualTransformation.None,
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
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
|
||||||
|
|
@ -22,6 +23,7 @@ import chat.revolt.R
|
||||||
import chat.revolt.api.REVOLT_SUPPORT
|
import chat.revolt.api.REVOLT_SUPPORT
|
||||||
import chat.revolt.api.routes.account.EmailPasswordAssessment
|
import chat.revolt.api.routes.account.EmailPasswordAssessment
|
||||||
import chat.revolt.api.routes.account.negotiateAuthentication
|
import chat.revolt.api.routes.account.negotiateAuthentication
|
||||||
|
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
|
||||||
|
|
@ -40,9 +42,9 @@ class LoginViewModel() : ViewModel() {
|
||||||
val error: String?
|
val error: String?
|
||||||
get() = _error
|
get() = _error
|
||||||
|
|
||||||
private var _navigateToMfa by mutableStateOf(false)
|
private var _navigateTo by mutableStateOf<String?>(null)
|
||||||
val navigateToMfa: Boolean
|
val navigateTo: String?
|
||||||
get() = _navigateToMfa
|
get() = _navigateTo
|
||||||
|
|
||||||
private var _mfaResponse by mutableStateOf<EmailPasswordAssessment?>(null)
|
private var _mfaResponse by mutableStateOf<EmailPasswordAssessment?>(null)
|
||||||
val mfaResponse: EmailPasswordAssessment?
|
val mfaResponse: EmailPasswordAssessment?
|
||||||
|
|
@ -60,19 +62,21 @@ class LoginViewModel() : ViewModel() {
|
||||||
if (response.proceedMfa) {
|
if (response.proceedMfa) {
|
||||||
Log.d("Login", "MFA required. Navigating to MFA screen")
|
Log.d("Login", "MFA required. Navigating to MFA screen")
|
||||||
_mfaResponse = response
|
_mfaResponse = response
|
||||||
_navigateToMfa = true
|
_navigateTo = "mfa"
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
Log.d(
|
||||||
"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)
|
||||||
|
_navigateTo = "home"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mfaComplete() {
|
fun navigationComplete() {
|
||||||
_navigateToMfa = false
|
_navigateTo = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setEmail(email: String) {
|
fun setEmail(email: String) {
|
||||||
|
|
@ -89,7 +93,7 @@ fun LoginScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: LoginViewModel = viewModel()
|
viewModel: LoginViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
if (viewModel.navigateToMfa) {
|
if (viewModel.navigateTo == "mfa") {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
"setup/mfa/${viewModel.mfaResponse!!.mfaSpec!!.ticket}/${
|
"setup/mfa/${viewModel.mfaResponse!!.mfaSpec!!.ticket}/${
|
||||||
viewModel.mfaResponse!!.mfaSpec!!.allowedMethods.joinToString(
|
viewModel.mfaResponse!!.mfaSpec!!.allowedMethods.joinToString(
|
||||||
|
|
@ -97,7 +101,12 @@ fun LoginScreen(
|
||||||
)
|
)
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
viewModel.mfaComplete()
|
viewModel.navigationComplete()
|
||||||
|
} else if (viewModel.navigateTo == "home") {
|
||||||
|
navController.navigate("chat/home") {
|
||||||
|
popUpTo("setup/greeting") { inclusive = true }
|
||||||
|
}
|
||||||
|
viewModel.navigationComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -143,7 +152,7 @@ fun LoginScreen(
|
||||||
FormTextField(
|
FormTextField(
|
||||||
value = viewModel.password,
|
value = viewModel.password,
|
||||||
label = stringResource(R.string.password),
|
label = stringResource(R.string.password),
|
||||||
password = true,
|
type = KeyboardType.Password,
|
||||||
onChange = { viewModel.setPassword(it) })
|
onChange = { viewModel.setPassword(it) })
|
||||||
|
|
||||||
AnyLink(
|
AnyLink(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
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
|
||||||
|
|
@ -197,6 +198,7 @@ fun MfaScreen(
|
||||||
FormTextField(
|
FormTextField(
|
||||||
label = stringResource(R.string.mfa_totp_code),
|
label = stringResource(R.string.mfa_totp_code),
|
||||||
onChange = { viewModel.setTotpCode(it) },
|
onChange = { viewModel.setTotpCode(it) },
|
||||||
|
type = KeyboardType.Number,
|
||||||
value = viewModel.totpCode,
|
value = viewModel.totpCode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue