mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
@@ -10,11 +10,10 @@ import io.ktor.client.plugins.websocket.WebSockets
|
|||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.URLBuilder
|
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.fromchat.API_HOST
|
import ru.fromchat.utils.failOnError
|
||||||
|
|
||||||
object ApiClient {
|
object ApiClient {
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
@@ -45,28 +44,21 @@ object ApiClient {
|
|||||||
var token: String? = null
|
var token: String? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private fun apiUrlBuilder(): URLBuilder {
|
suspend fun login(request: LoginRequest) =
|
||||||
return URLBuilder().apply {
|
http
|
||||||
protocol = io.ktor.http.URLProtocol.HTTPS
|
.post("https://fromchat.ru/api/login") {
|
||||||
host = API_HOST
|
contentType(ContentType.Application.Json)
|
||||||
}
|
setBody(request)
|
||||||
}
|
}
|
||||||
|
.failOnError()
|
||||||
|
.body<LoginResponse>()
|
||||||
|
.also { token = it.token }
|
||||||
|
|
||||||
suspend fun login(request: LoginRequest): LoginResponse {
|
suspend fun register(request: RegisterRequest) =
|
||||||
// Endpoint exists in web: POST /login with {username,password}
|
http
|
||||||
val response = http.post("https://fromchat.ru/api/login") {
|
.post("https://fromchat.ru/api/register") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(request)
|
setBody(request)
|
||||||
}.body<LoginResponse>()
|
}
|
||||||
token = response.token
|
.failOnError()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ data class RegisterRequest(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ErrorResponse(
|
data class ErrorResponse(
|
||||||
val message: String? = null
|
val detail: String
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ru.fromchat.api
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.plugins.ClientRequestException
|
||||||
|
|
||||||
|
suspend inline fun <Response> apiRequest(
|
||||||
|
onError: (String, Exception) -> Unit,
|
||||||
|
onSuccess: (Response) -> Unit,
|
||||||
|
request: suspend () -> Response
|
||||||
|
): Result<Response> {
|
||||||
|
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<ErrorResponse>().detail
|
||||||
|
} else {
|
||||||
|
"Unexpected error"
|
||||||
|
},
|
||||||
|
e
|
||||||
|
)
|
||||||
|
return Result.failure(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onError("Unexpected error", e)
|
||||||
|
return Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package ru.fromchat.ui.auth
|
package ru.fromchat.ui.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
@@ -16,16 +17,16 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.LoginRequest
|
import ru.fromchat.api.LoginRequest
|
||||||
|
import ru.fromchat.api.apiRequest
|
||||||
import ru.fromchat.ui.RowHeader
|
import ru.fromchat.ui.RowHeader
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -38,6 +39,8 @@ fun LoginScreen(
|
|||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var alert by remember { mutableStateOf<String?>(null) }
|
var alert by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -84,12 +87,15 @@ fun LoginScreen(
|
|||||||
return@Button
|
return@Button
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
scope.launch {
|
||||||
try {
|
apiRequest(
|
||||||
|
onError = { message, e ->
|
||||||
|
Log.e("LoginScreen", "An error ocurred while logging in:", e)
|
||||||
|
alert = message
|
||||||
|
},
|
||||||
|
onSuccess = { onLoginSuccess() }
|
||||||
|
) {
|
||||||
ApiClient.login(LoginRequest(username, password))
|
ApiClient.login(LoginRequest(username, password))
|
||||||
onLoginSuccess()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
alert = "Неверное имя пользователя или пароль"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.RegisterRequest
|
import ru.fromchat.api.RegisterRequest
|
||||||
|
import ru.fromchat.api.apiRequest
|
||||||
import ru.fromchat.ui.RowHeader
|
import ru.fromchat.ui.RowHeader
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -38,6 +38,8 @@ fun RegisterScreen(
|
|||||||
var confirmPassword by remember { mutableStateOf("") }
|
var confirmPassword by remember { mutableStateOf("") }
|
||||||
var alert by remember { mutableStateOf<String?>(null) }
|
var alert by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -105,8 +107,14 @@ fun RegisterScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send the register request
|
// Send the register request
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
scope.launch {
|
||||||
try {
|
apiRequest(
|
||||||
|
onError = { message, e ->
|
||||||
|
Log.d("RegisterScreen", "Error while registering:", e)
|
||||||
|
alert = message
|
||||||
|
},
|
||||||
|
onSuccess = { onRegistered() }
|
||||||
|
) {
|
||||||
ApiClient.register(
|
ApiClient.register(
|
||||||
RegisterRequest(
|
RegisterRequest(
|
||||||
username,
|
username,
|
||||||
@@ -114,10 +122,6 @@ fun RegisterScreen(
|
|||||||
confirmPassword
|
confirmPassword
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
onRegistered()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("RegisterScreen", "Error while registering:", e)
|
|
||||||
alert = "Ошибка при регистрации"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user