diff --git a/app/src/main/java/ru/fromchat/api/Http.kt b/app/src/main/java/ru/fromchat/api/Http.kt index 0ccdc68..c1e2da7 100644 --- a/app/src/main/java/ru/fromchat/api/Http.kt +++ b/app/src/main/java/ru/fromchat/api/Http.kt @@ -10,11 +10,10 @@ import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.ContentType -import io.ktor.http.URLBuilder import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import ru.fromchat.API_HOST +import ru.fromchat.utils.failOnError object ApiClient { private val json = Json { @@ -45,28 +44,21 @@ object ApiClient { var token: String? = null private set - private fun apiUrlBuilder(): URLBuilder { - return URLBuilder().apply { - protocol = io.ktor.http.URLProtocol.HTTPS - host = API_HOST - } - } + suspend fun login(request: LoginRequest) = + http + .post("https://fromchat.ru/api/login") { + contentType(ContentType.Application.Json) + setBody(request) + } + .failOnError() + .body() + .also { token = it.token } - suspend fun login(request: LoginRequest): LoginResponse { - // Endpoint exists in web: POST /login with {username,password} - val response = http.post("https://fromchat.ru/api/login") { - contentType(ContentType.Application.Json) - setBody(request) - }.body() - token = response.token - return response - } - - suspend fun register(request: RegisterRequest) { - // Endpoint exists in web: POST /register with {username,password,confirm_password} - http.post("https://fromchat.ru/api/register") { - contentType(ContentType.Application.Json) - setBody(request) - } - } + suspend fun register(request: RegisterRequest) = + http + .post("https://fromchat.ru/api/register") { + contentType(ContentType.Application.Json) + setBody(request) + } + .failOnError() } \ No newline at end of file diff --git a/app/src/main/java/ru/fromchat/api/Models.kt b/app/src/main/java/ru/fromchat/api/Models.kt index eb4ffcf..d93ac1d 100644 --- a/app/src/main/java/ru/fromchat/api/Models.kt +++ b/app/src/main/java/ru/fromchat/api/Models.kt @@ -17,7 +17,7 @@ data class RegisterRequest( @Serializable data class ErrorResponse( - val message: String? = null + val detail: String ) @Serializable diff --git a/app/src/main/java/ru/fromchat/api/Utils.kt b/app/src/main/java/ru/fromchat/api/Utils.kt new file mode 100644 index 0000000..cadbb90 --- /dev/null +++ b/app/src/main/java/ru/fromchat/api/Utils.kt @@ -0,0 +1,29 @@ +package ru.fromchat.api + +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException + +suspend inline fun apiRequest( + onError: (String, Exception) -> Unit, + onSuccess: (Response) -> Unit, + request: suspend () -> Response +): Result { + try { + val response = request() + onSuccess(response) + return Result.success(response) + } catch (e: ClientRequestException) { + onError( + if (e.response.status.value in arrayOf(401, 403)) { + e.response.body().detail + } else { + "Unexpected error" + }, + e + ) + return Result.failure(e) + } catch (e: Exception) { + onError("Unexpected error", e) + return Result.failure(e) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/fromchat/ui/auth/LoginScreen.kt b/app/src/main/java/ru/fromchat/ui/auth/LoginScreen.kt index 5e0ee7b..e84dc64 100644 --- a/app/src/main/java/ru/fromchat/ui/auth/LoginScreen.kt +++ b/app/src/main/java/ru/fromchat/ui/auth/LoginScreen.kt @@ -1,5 +1,6 @@ package ru.fromchat.ui.auth +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow @@ -16,16 +17,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ru.fromchat.api.ApiClient import ru.fromchat.api.LoginRequest +import ru.fromchat.api.apiRequest import ru.fromchat.ui.RowHeader @Composable @@ -38,6 +39,8 @@ fun LoginScreen( var password by remember { mutableStateOf("") } var alert by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + Column( modifier = Modifier .fillMaxSize() @@ -84,12 +87,15 @@ fun LoginScreen( return@Button } - CoroutineScope(Dispatchers.IO).launch { - try { + scope.launch { + apiRequest( + onError = { message, e -> + Log.e("LoginScreen", "An error ocurred while logging in:", e) + alert = message + }, + onSuccess = { onLoginSuccess() } + ) { ApiClient.login(LoginRequest(username, password)) - onLoginSuccess() - } catch (e: Exception) { - alert = "Неверное имя пользователя или пароль" } } } diff --git a/app/src/main/java/ru/fromchat/ui/auth/RegisterScreen.kt b/app/src/main/java/ru/fromchat/ui/auth/RegisterScreen.kt index 37b3c18..04b7bbe 100644 --- a/app/src/main/java/ru/fromchat/ui/auth/RegisterScreen.kt +++ b/app/src/main/java/ru/fromchat/ui/auth/RegisterScreen.kt @@ -16,16 +16,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ru.fromchat.api.ApiClient import ru.fromchat.api.RegisterRequest +import ru.fromchat.api.apiRequest import ru.fromchat.ui.RowHeader @Composable @@ -38,6 +38,8 @@ fun RegisterScreen( var confirmPassword by remember { mutableStateOf("") } var alert by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + Column( modifier = Modifier .fillMaxSize() @@ -105,8 +107,14 @@ fun RegisterScreen( } // Send the register request - CoroutineScope(Dispatchers.IO).launch { - try { + scope.launch { + apiRequest( + onError = { message, e -> + Log.d("RegisterScreen", "Error while registering:", e) + alert = message + }, + onSuccess = { onRegistered() } + ) { ApiClient.register( RegisterRequest( username, @@ -114,10 +122,6 @@ fun RegisterScreen( confirmPassword ) ) - onRegistered() - } catch (e: Exception) { - Log.d("RegisterScreen", "Error while registering:", e) - alert = "Ошибка при регистрации" } } } diff --git a/app/src/main/java/ru/fromchat/utils/Ktor.kt b/app/src/main/java/ru/fromchat/utils/Ktor.kt new file mode 100644 index 0000000..d7d8565 --- /dev/null +++ b/app/src/main/java/ru/fromchat/utils/Ktor.kt @@ -0,0 +1,102 @@ +@file:Suppress("NOTHING_TO_INLINE") +package ru.fromchat.utils + +import android.util.Log +import io.ktor.client.HttpClientConfig +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.DefaultRequest +import io.ktor.client.plugins.ResponseException +import io.ktor.client.plugins.ServerResponseException +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.DEFAULT +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.logging.LoggingConfig +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.serialization.Configuration +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonBuilder + +/** + * Configures the Ktor client for kotlinx.serialization JSON with custom settings. + * @param settings Lambda to configure the [JsonBuilder]. + */ +@PublishedApi +internal inline fun Configuration.json(crossinline settings: JsonBuilder.() -> Unit) { + json( + Json { + settings() + } + ) +} + +/** + * Installs [ContentNegotiation] with JSON support and custom settings in a Ktor client config. + * @param settings Lambda to configure the [JsonBuilder]. + */ +inline fun HttpClientConfig<*>.jsonConfig( + crossinline settings: JsonBuilder.() -> Unit +) = install(ContentNegotiation) { + json { + settings() + } +} + +/** + * Installs [DefaultRequest] in a Ktor client config. + * @param settings Lambda to configure the [DefaultRequest.DefaultRequestBuilder]. + */ +inline fun HttpClientConfig<*>.defaultRequest( + crossinline settings: DefaultRequest.DefaultRequestBuilder.() -> Unit +) = install(DefaultRequest) { + settings() +} + +/** + * Installs [Logging] in a Ktor client config. + * @param settings Lambda to configure the [LoggingConfig]. + */ +inline fun HttpClientConfig<*>.logging( + crossinline settings: LoggingConfig.() -> Unit +) = install(Logging) { + settings() +} + +/** + * Installs [Logging] in a Ktor client config with default settings (all logs). + */ +inline fun HttpClientConfig<*>.logging() = logging { + logger = Logger.DEFAULT + level = LogLevel.ALL +} + +/** + * Throws if the HTTP response status code is an error (>=400). + * @return The [HttpResponse] if successful. + * @throws ClientRequestException + * @throws ServerResponseException + * @throws ResponseException on error. + */ +suspend fun HttpResponse.failOnError(): HttpResponse { + Log.d("failOnError", "Checking the response code...") + val statusCode = status.value + suspend fun body() = bodyAsText() + + Log.d("failOnError", "code: $statusCode") + + when (statusCode) { + in 400..499 -> throw ClientRequestException(this, body()) + in 500..599 -> throw ServerResponseException(this, body()) + } + + if (statusCode >= 600) { + throw ResponseException(this, body()) + } + + Log.d("failOnError", "Success") + + return this +} \ No newline at end of file