Skip to content
Merged

dev #16

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions commonApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ kotlin {
implementation(project(":core:paging"))
implementation(project(":core:todo"))
implementation(project(":core:auth"))
implementation(project(":core:network:api"))
implementation(project(":core:network:client"))
implementation(project(":core:store"))

implementation(project(":feature:home"))
implementation(project(":feature:details"))
Expand Down
2 changes: 2 additions & 0 deletions commonApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".AtTenApp"
android:allowBackup="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.stslex.atten.di
import com.stslex.atten.core.auth.di.ModuleCoreAuth
import com.stslex.atten.core.core.di.ModuleCore
import com.stslex.atten.core.database.di.ModuleCoreDatabase
import com.stslex.atten.core.network.client.di.ModuleCoreNetwork
import com.stslex.atten.core.paging.di.ModuleCorePaging
import com.stslex.atten.core.todo.di.ModuleCoreToDo
import com.stslex.atten.core.ui.kit.utils.ModuleCoreUiUtils
Expand All @@ -19,6 +20,7 @@ val appModules: List<Module> = listOf(
ModuleCorePaging().module,
ModuleCoreUiUtils().module,
ModuleCoreAuth().module,
ModuleCoreNetwork().module,
ModuleFeatureHome().module,
ModuleFeatureDetails().module,
ModuleFeatureSettings().module
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stslex.atten.core.core.model

open class AppError(
message: String? = null,
cause: Throwable? = null
) : Throwable(
message = message,
cause = cause,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package com.stslex.atten.core.core.model

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import androidx.sqlite.execSQL
import com.stslex.atten.core.database.model.ToDoEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.datetime.Clock
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

@Database(
entities = [ToDoEntity::class],
Expand All @@ -25,6 +26,7 @@ abstract class AppDatabase : RoomDatabase() {
}
}

@OptIn(ExperimentalTime::class)
fun getRoomDatabase(
builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
Expand Down
40 changes: 40 additions & 0 deletions core/network/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import com.github.gmazzo.buildconfig.BuildConfigExtension
import java.util.Properties

plugins {
alias(libs.plugins.convention.kmp.library)
alias(libs.plugins.buildConfig)
}

kotlin {
sourceSets.apply {
commonMain {
dependencies {
implementation(project(":core:core"))
implementation(project(":core:store"))
implementation(libs.bundles.ktor)
}
}
buildConfig {
setLocalProperty(project.rootProject)
}
}
}

fun BuildConfigExtension.setLocalProperty(dir: Project) {
val localProperties = gradleLocalProperties(dir.projectDir, providers)

buildStringField(localProperties, "SERVER_HOST")
buildStringField(localProperties, "SERVER_API_VERSION")
buildStringField(localProperties, "SERVER_PORT")
buildStringField(localProperties, "SERVER_API_KEY")
}

fun BuildConfigExtension.buildStringField(localProperties: Properties, name: String) {
buildConfigField("String", name, localProperties.getString(name))
}

fun Properties.getString(key: String): String {
return getProperty(key) ?: throw IllegalStateException("$key should be initialised")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.stslex.atten.core.network.api

import io.ktor.client.HttpClient

interface AppHttpApi {

suspend fun <T> request(block: suspend HttpClient.() -> T): T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.stslex.atten.core.network.api

import io.ktor.client.HttpClient

interface AppHttpClient {

val client: HttpClient
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.stslex.atten.core.network.api

import com.stslex.atten.core.network.api.model.TokenResponseModel

interface AuthApiClient {

suspend fun auth(token: String): TokenResponseModel

suspend fun refresh(): TokenResponseModel
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.stslex.atten.core.network.api

import io.ktor.client.plugins.CallRequestExceptionHandler

interface ErrorHandler : CallRequestExceptionHandler
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stslex.atten.core.network.api.model

import com.stslex.atten.core.core.model.AppError

sealed class NetworkError(
message: String,
cause: Throwable? = null
) : AppError(
message = message,
cause = cause,
) {

data class ErrorRefresh(
override val cause: Throwable,
) : NetworkError("success token not valid", cause)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stslex.atten.core.network.api.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class TokenResponseModel(
@SerialName("uuid")
val uuid: String,
@SerialName("email")
val email: String,
@SerialName("access_token")
val accessToken: String,
@SerialName("refresh_token")
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.stslex.atten.core.network.api.model.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class AuthGoogleRequest(
@SerialName("id_token")
val idToken: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stslex.atten.core.network.api.model.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class LoginOkResponse(
@SerialName("uuid")
val uuid: String,
@SerialName("email")
val email: String,
@SerialName("access_token")
val accessToken: String,
@SerialName("refresh_token")
val refreshToken: String
)
41 changes: 41 additions & 0 deletions core/network/client/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import com.github.gmazzo.buildconfig.BuildConfigExtension
import java.util.Properties

plugins {
alias(libs.plugins.convention.kmp.library)
alias(libs.plugins.buildConfig)
}

kotlin {
sourceSets.apply {
commonMain {
dependencies {
implementation(project(":core:core"))
implementation(project(":core:network:api"))
implementation(project(":core:store"))
implementation(libs.bundles.ktor)
}
}
buildConfig {
setLocalProperty(project.rootProject)
}
}
}

fun BuildConfigExtension.setLocalProperty(dir: Project) {
val localProperties = gradleLocalProperties(dir.projectDir, providers)

buildStringField(localProperties, "SERVER_HOST")
buildStringField(localProperties, "SERVER_API_VERSION")
buildStringField(localProperties, "SERVER_PORT")
buildStringField(localProperties, "SERVER_API_KEY")
}

fun BuildConfigExtension.buildStringField(localProperties: Properties, name: String) {
buildConfigField("String", name, localProperties.getString(name))
}

fun Properties.getString(key: String): String {
return getProperty(key) ?: throw IllegalStateException("$key should be initialised")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.stslex.atten.core.network.client.client

import com.stslex.atten.core.core.coroutine.dispatcher.AppDispatcher
import com.stslex.atten.core.network.api.AppHttpApi
import com.stslex.atten.core.network.api.AppHttpClient
import io.ktor.client.HttpClient
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Single
import org.koin.core.annotation.Singleton

@Single
@Singleton
class AppHttpApiImpl(
private val appDispatcher: AppDispatcher,
private val appHttpClient: AppHttpClient
) : AppHttpApi {

override suspend fun <T> request(
block: suspend HttpClient.() -> T
): T = withContext(appDispatcher.io) {
block(appHttpClient.client)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.stslex.atten.core.network.client.client

import AtTen.core.network.client.BuildConfig
import com.stslex.atten.core.network.api.AppHttpClient
import com.stslex.atten.core.network.api.ErrorHandler
import com.stslex.atten.core.network.client.error.KtorLogger
import com.stslex.atten.core.store.user.UserStore
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.cio.CIO
import io.ktor.client.engine.cio.CIOEngineConfig
import io.ktor.client.plugins.HttpResponseValidator
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.headers
import io.ktor.http.ContentType
import io.ktor.http.URLProtocol
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import org.koin.core.annotation.Single
import org.koin.core.annotation.Singleton

@Single
@Singleton
internal class AppHttpClientImpl(
errorHandler: ErrorHandler,
private val userStore: UserStore,
) : AppHttpClient {

override val client = HttpClient(CIO) {
install(HttpCache.Companion)
expectSuccess = true
HttpResponseValidator { handleResponseExceptionWithRequest(errorHandler) }
setupDefaultRequest()
setupNegotiation()
setupLogging()
installAuth()
}

private fun HttpClientConfig<CIOEngineConfig>.setupNegotiation() {
install(ContentNegotiation) {
json(
Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
explicitNulls = false
}
)
}
}

private fun HttpClientConfig<CIOEngineConfig>.setupLogging() {
install(Logging) {
// TODO dev type log
logger = KtorLogger
level = LogLevel.ALL
}
}

private fun HttpClientConfig<*>.installAuth() {
install(Auth) {
bearer {
loadTokens {
BearerTokens(
accessToken = userStore.accessToken.value,
refreshToken = userStore.refreshToken.value
)
}
}
}
}

private fun HttpClientConfig<*>.setupDefaultRequest() {
defaultRequest {
url(
scheme = URLProtocol.Companion.HTTP.name,
host = BuildConfig.SERVER_HOST,
port = BuildConfig.SERVER_PORT.toInt(),
path = BuildConfig.SERVER_API_VERSION,
block = {
contentType(ContentType.Application.Json)
}
)
headers {
append(
API_KEY_NAME,
BuildConfig.SERVER_API_KEY
)
}
}
}

companion object {
private const val API_KEY_NAME = "X-Api-Key"
}
}
Loading
Loading