Implement sign in with api key
This commit is contained in:
+4
-6
@@ -1,14 +1,12 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp
|
||||
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.AuthApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.Endpoints
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
||||
import com.digitoolsolutions.app.torquevaultkmp.utils.ForceLogoutException
|
||||
import io.github.aakira.napier.Napier
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.DefaultRequest
|
||||
import io.ktor.client.plugins.HttpSend
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
@@ -17,9 +15,6 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.plugins.plugin
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@@ -45,6 +40,9 @@ actual fun createPlatformHttpClient(
|
||||
} else null
|
||||
}
|
||||
refreshTokens {
|
||||
if (response.call.request.url.encodedPath.contains(Endpoints.AUTH_TOKEN)) {
|
||||
return@refreshTokens null
|
||||
}
|
||||
Napier.d(">>>>> [NetworkEngine.android.kt] Access token expired, auto request refresh token...")
|
||||
val newAccess = authApi?.refreshToken()
|
||||
if (newAccess != null) {
|
||||
|
||||
@@ -19,11 +19,15 @@
|
||||
<string name="dark_theme">Dark theme</string>
|
||||
<string name="auto_connect">Auto-connect device</string>
|
||||
|
||||
<string name="lbl_server_url">Server URL</string>
|
||||
<string name="lbl_server_url">Server Address</string>
|
||||
<string name="server_url_placeholder">Ex: https://example.com</string>
|
||||
<string name="lbl_username">Username</string>
|
||||
<string name="lbl_password">Password</string>
|
||||
<string name="btn_login">Login</string>
|
||||
<string name="btn_login">Sign In</string>
|
||||
<string name="api_key">API Key</string>
|
||||
<string name="use_api_key_instead">Use API key instead?</string>
|
||||
<string name="use_password_instead">Use password instead?</string>
|
||||
|
||||
|
||||
<string name="bonded_title">Bonded Devices</string>
|
||||
|
||||
|
||||
+21
@@ -8,9 +8,12 @@ import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
||||
import io.github.aakira.napier.Napier
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.contentType
|
||||
|
||||
|
||||
@@ -42,6 +45,24 @@ class AuthApi(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to authenticate the user using an API Key.
|
||||
* Calls Endpoints.AUTH_REFRESH and stores the API Key as the access token.
|
||||
*/
|
||||
suspend fun loginWithApiKey(apiKey: String): Boolean {
|
||||
return try {
|
||||
val res: RefreshResponseDto = client.post("$baseUrl${Endpoints.AUTH_REFRESH}") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(RefreshRequestDto(apiKey))
|
||||
}.body()
|
||||
tokenManager.saveTokens(res.access, apiKey)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Napier.d(">>>>> [AuthApi.kt] loginWithApiKey Exception: $e")
|
||||
false
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Attempts to refresh the access token using the stored refresh token.
|
||||
*
|
||||
|
||||
+21
-1
@@ -60,7 +60,27 @@ class AuthViewModel(
|
||||
if (success) {
|
||||
_loginState.value = LoginUiState.Success
|
||||
} else {
|
||||
_loginState.value = LoginUiState.Error("Login failed: Invalid credentials")
|
||||
_loginState.value = LoginUiState.Error("Incorrect username or password")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_loginState.value = LoginUiState.Error("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to log in the user using an API Key.
|
||||
*/
|
||||
fun loginWithApiKey(apiKey: String) {
|
||||
viewModelScope.launch {
|
||||
_loginState.value = LoginUiState.Loading
|
||||
tokenManager.saveServerUrl(_serverUrl.value)
|
||||
try {
|
||||
val success = authApi.loginWithApiKey(apiKey)
|
||||
if (success) {
|
||||
_loginState.value = LoginUiState.Success
|
||||
} else {
|
||||
_loginState.value = LoginUiState.Error("Invalid API Key")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_loginState.value = LoginUiState.Error("Error: ${e.message}")
|
||||
|
||||
+65
-8
@@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -33,6 +34,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -53,7 +55,6 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeDestination
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -61,12 +62,15 @@ import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import torquevaultkmp.composeapp.generated.resources.Res
|
||||
import torquevaultkmp.composeapp.generated.resources.api_key
|
||||
import torquevaultkmp.composeapp.generated.resources.btn_login
|
||||
import torquevaultkmp.composeapp.generated.resources.dts_text
|
||||
import torquevaultkmp.composeapp.generated.resources.lbl_password
|
||||
import torquevaultkmp.composeapp.generated.resources.lbl_server_url
|
||||
import torquevaultkmp.composeapp.generated.resources.lbl_username
|
||||
import torquevaultkmp.composeapp.generated.resources.server_url_placeholder
|
||||
import torquevaultkmp.composeapp.generated.resources.use_api_key_instead
|
||||
import torquevaultkmp.composeapp.generated.resources.use_password_instead
|
||||
|
||||
/**
|
||||
* Screen for user authentication.
|
||||
@@ -80,12 +84,15 @@ fun LoginScreen(
|
||||
val serverUrl by viewModel.serverUrl.collectAsState()
|
||||
|
||||
LoginScreenContent(
|
||||
viewModel = viewModel,
|
||||
loginState = loginState,
|
||||
serverUrl = serverUrl,
|
||||
onServerUrlChange = { viewModel.onChangeServerUrl(it) },
|
||||
onLogin = { username, password ->
|
||||
viewModel.login(username, password)
|
||||
},
|
||||
onLoginWithApiKey = { apiKey ->
|
||||
viewModel.loginWithApiKey(apiKey)
|
||||
},
|
||||
onLoginSuccess = {
|
||||
navController.navigate(HomeDestination.route) {
|
||||
popUpTo(AuthDestination.route) { inclusive = true }
|
||||
@@ -101,16 +108,19 @@ fun LoginScreen(
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoginScreenContent(
|
||||
viewModel: AuthViewModel,
|
||||
loginState: LoginUiState,
|
||||
serverUrl: String,
|
||||
onServerUrlChange: (String) -> Unit,
|
||||
onLogin: (String, String) -> Unit,
|
||||
onLoginWithApiKey: (String) -> Unit,
|
||||
onLoginSuccess: () -> Unit
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
var username by remember { mutableStateOf("admin") }
|
||||
var password by remember { mutableStateOf("admin") }
|
||||
var apiKey by remember { mutableStateOf("") }
|
||||
var useApiKey by remember { mutableStateOf(false) }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -165,12 +175,14 @@ fun LoginScreenContent(
|
||||
onNext = { focusManager.moveFocus(FocusDirection.Down) }
|
||||
),
|
||||
value = serverUrl,
|
||||
onValueChange = { viewModel.onChangeServerUrl(it) },
|
||||
onValueChange = { onServerUrlChange(it) },
|
||||
label = { Text(stringResource(Res.string.lbl_server_url)) },
|
||||
placeholder = { Text(stringResource(Res.string.server_url_placeholder)) }
|
||||
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (!useApiKey) {
|
||||
/* Use username and password */
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
@@ -215,12 +227,46 @@ fun LoginScreenContent(
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(Res.string.lbl_password)) }
|
||||
)
|
||||
} else {
|
||||
/* Use api key */
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
if (apiKey.isBlank()) {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("API Key is required!")
|
||||
}
|
||||
} else {
|
||||
onLoginWithApiKey(apiKey)
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
),
|
||||
value = apiKey,
|
||||
onValueChange = { apiKey = it },
|
||||
label = { Text(stringResource(Res.string.api_key)) }
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.width(150.dp)
|
||||
.height(45.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(45.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
onClick = {
|
||||
if (useApiKey) {
|
||||
if (apiKey.isBlank()) {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("API Key is required!")
|
||||
}
|
||||
} else {
|
||||
onLoginWithApiKey(apiKey.trim())
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
} else {
|
||||
if (username.isBlank() || password.isBlank()) {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("Username and password is required!")
|
||||
@@ -230,6 +276,7 @@ fun LoginScreenContent(
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = loginState !is LoginUiState.Loading
|
||||
) {
|
||||
@@ -243,6 +290,15 @@ fun LoginScreenContent(
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
onClick = { useApiKey = !useApiKey },
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(stringResource(if (useApiKey) Res.string.use_password_instead else Res.string.use_api_key_instead))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,10 +307,11 @@ fun LoginScreenContent(
|
||||
@Composable
|
||||
private fun LoginScreenPreview() {
|
||||
LoginScreenContent(
|
||||
viewModel = viewModel(),
|
||||
loginState = LoginUiState.Idle,
|
||||
serverUrl = "http://localhost:3000",
|
||||
onServerUrlChange = {},
|
||||
onLogin = {_, _ -> },
|
||||
onLoginWithApiKey = {},
|
||||
onLoginSuccess = {}
|
||||
)
|
||||
}
|
||||
|
||||
+4
-7
@@ -1,14 +1,12 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp
|
||||
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.AuthApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.Endpoints
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
||||
import com.digitoolsolutions.app.torquevaultkmp.utils.ForceLogoutException
|
||||
import io.github.aakira.napier.Napier
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.darwin.Darwin
|
||||
import io.ktor.client.plugins.DefaultRequest
|
||||
import io.ktor.client.plugins.HttpSend
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
@@ -17,11 +15,7 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.plugins.plugin
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
actual fun createPlatformHttpClient(
|
||||
@@ -45,6 +39,9 @@ actual fun createPlatformHttpClient(
|
||||
} else null
|
||||
}
|
||||
refreshTokens {
|
||||
if (response.call.request.url.encodedPath.contains(Endpoints.AUTH_TOKEN)) {
|
||||
return@refreshTokens null
|
||||
}
|
||||
Napier.d(">>>>> [NetworkEngine.ios.kt] Access token expired, auto request refresh token...")
|
||||
val newAccess = authApi?.refreshToken()
|
||||
if (newAccess != null) {
|
||||
|
||||
Reference in New Issue
Block a user