Skip to content
Merged
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: 0 additions & 3 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/network-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ android {
}

dependencies {

implementation(libs.gson)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
Expand Down
12 changes: 12 additions & 0 deletions core/network-api/src/main/java/ru/yeahub/network_api/ApiService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@ import ru.yeahub.network_api.models.GetSkillsResponse
import ru.yeahub.network_api.models.GetSpecializationResponse
import ru.yeahub.network_api.models.GetSpecializationsResponse
import ru.yeahub.network_api.models.RegistrationRequestDto
import ru.yeahub.network_api.models.AuthUserDto
import ru.yeahub.network_api.models.LoginRequestDto
import ru.yeahub.network_api.models.LoginResponseDto

interface ApiService {

/**
* API авторизации:
* - login - выполняет вход по email и паролю
* - getProfile - получает профиль авторизованного пользователя
*/
suspend fun login(request: LoginRequestDto): LoginResponseDto

suspend fun getProfile(): AuthUserDto

suspend fun register(request: RegistrationRequestDto)

suspend fun getQuestions(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ru.yeahub.network_api.models

/**
* DTO разрешения пользователя:
* - id - идентификатор разрешения
* - name - название разрешения
*/
data class AuthPermissionDto(
val id: Int,
val name: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ru.yeahub.network_api.models

/**
* DTO пользователя из auth response:
* - id - идентификатор пользователя
* - username - имя пользователя
* - phone - телефон
* - email - email
* - country - страна
* - city - город
* - address - адрес
* - avatarUrl - ссылка на аватар
* - birthday - дата рождения
* - updatedAt - дата обновления
* - createdAt - дата создания
* - userRoles - роли пользователя
* - isVerified - подтверждён ли пользователь
* - isEmailNotificationsEnable - включены ли email-уведомления
*/
data class AuthUserDto(
val id: String?,
val username: String?,
val phone: String?,
val email: String?,
val country: String?,
val city: String?,
val address: String?,
val avatarUrl: String?,
val birthday: String?,
val updatedAt: String?,
val createdAt: String?,
val userRoles: List<AuthUserRoleDto>?,
val isVerified: Boolean?,
val isEmailNotificationsEnable: Boolean?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ru.yeahub.network_api.models

/**
* DTO роли пользователя:
* - id - идентификатор роли
* - name - название роли
* - permissions - список разрешений роли
*/
data class AuthUserRoleDto(
val id: Int,
val name: String,
val permissions: List<AuthPermissionDto>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ru.yeahub.network_api.models

/**
* Response модель ошибки backend:
* - key - backend key ошибки
* - message - текстовое описание ошибки
*/
data class ErrorResponseDto(
val key: String?,
val message: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ru.yeahub.network_api.models

/**
* Request модель авторизации:
* - email - email пользователя
* - password - пароль пользователя
*/
data class LoginRequestDto(
val email: String,
val password: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ru.yeahub.network_api.models

import com.google.gson.annotations.SerializedName
/**
* Response модель авторизации:
* - accessToken - access token пользователя
* - user - данные пользователя
*/
data class LoginResponseDto(
@SerializedName("access_token")
val accessToken: String,
val user: AuthUserDto,
)
1 change: 1 addition & 0 deletions feature/authentication/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies {

// Retrofit (for HttpException)
implementation(libs.retrofit.core)
implementation(libs.gson)

debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ru.yeahub.authentication.impl.login.data.mapper

import ru.yeahub.authentication.impl.login.domain.entity.LoginModel
import ru.yeahub.network_api.models.LoginRequestDto

/**
* Mapper domain модели авторизации в request модель backend:
* - LoginModel преобразует в LoginRequestDto
*/
class LoginDomainToDataMapper {

fun map(model: LoginModel): LoginRequestDto {
return LoginRequestDto(
email = model.email,
password = model.password,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ru.yeahub.authentication.impl.login.data.mapper

import ru.yeahub.authentication.impl.login.domain.entity.AuthResult
import ru.yeahub.authentication.impl.login.domain.entity.AuthTokens
import ru.yeahub.authentication.impl.login.domain.entity.UserProfile
import ru.yeahub.network_api.models.LoginResponseDto

/**
* Mapper response модели авторизации в domain модель:
* - LoginResponseDto преобразует в AuthResult
*/
class LoginResponseToDomainMapper {

fun map(dto: LoginResponseDto): AuthResult {
return AuthResult(
tokens = AuthTokens(
accessToken = dto.accessToken,
),
userProfile = UserProfile(
id = dto.user.id,
email = dto.user.email,
username = dto.user.username,
avatarUrl = dto.user.avatarUrl,
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package ru.yeahub.authentication.impl.login.data.repository

import com.google.gson.Gson
import kotlinx.coroutines.CancellationException
import retrofit2.HttpException
import ru.yeahub.authentication.impl.login.data.mapper.LoginDomainToDataMapper
import ru.yeahub.authentication.impl.login.data.mapper.LoginResponseToDomainMapper
import ru.yeahub.authentication.impl.login.data.repository.remote.LoginRemoteDataSourceApi
import ru.yeahub.authentication.impl.login.domain.entity.AuthResult
import ru.yeahub.authentication.impl.login.domain.entity.Failure
import ru.yeahub.authentication.impl.login.domain.entity.LoginError
import ru.yeahub.authentication.impl.login.domain.entity.LoginException
import ru.yeahub.authentication.impl.login.domain.entity.LoginModel
import ru.yeahub.authentication.impl.login.domain.repository.LoginRepositoryApi
import ru.yeahub.network_api.models.ErrorResponseDto
import java.io.IOException

/**
* Реализация репозитория авторизации:
* - преобразует domain модель в DTO
* - вызывает remote data source
* - преобразует DTO в domain модель
* - маппит сетевые и backend ошибки в LoginException
*/
class LoginRepositoryImpl(
private val remoteDataSourceApi: LoginRemoteDataSourceApi,
private val domainToDataMapper: LoginDomainToDataMapper,
private val responseToDomainMapper: LoginResponseToDomainMapper,
private val gson: Gson,
) : LoginRepositoryApi {

override suspend fun login(loginModel: LoginModel): AuthResult {
return try {
val request = domainToDataMapper.map(loginModel)
val response = remoteDataSourceApi.login(request)
responseToDomainMapper.map(response)
} catch (exception: CancellationException) {
throw exception
} catch (exception: IOException) {
throw LoginException(
error = LoginError.Network,
failure = Failure(cause = exception),
)
} catch (exception: HttpException) {
throw mapHttpException(exception)
}
}

/**
* Маппит HTTP-ошибку backend в domain ошибку авторизации:
* - сначала пытается прочитать backend key
* - если key неизвестен, использует HTTP status code
*/
private fun mapHttpException(exception: HttpException): LoginException {
val code = exception.code()
val backendKey = exception.getBackendErrorKey()

val error = when (backendKey) {
BACKEND_KEY_INVALID_PASSWORD -> LoginError.InvalidPassword
BACKEND_KEY_INVALID_CREDENTIALS -> LoginError.InvalidCredentials
BACKEND_KEY_USER_NOT_FOUND -> LoginError.UserNotFound
BACKEND_KEY_ACCOUNT_BLOCKED -> LoginError.AccountBlocked
BACKEND_KEY_TOO_MANY_ATTEMPTS -> LoginError.TooManyAttempts
BACKEND_KEY_EMAIL_NOT_CONFIRMED -> LoginError.EmailNotConfirmed
else -> mapHttpCodeToError(code)
}

return LoginException(
error = error,
failure = Failure(
cause = exception,
httpCode = code,
),
)
}

/**
* Маппит HTTP status code в domain ошибку авторизации:
* - используется как fallback, если backend key отсутствует или неизвестен
*/
private fun mapHttpCodeToError(code: Int): LoginError {
return when (code) {
HTTP_BAD_REQUEST,
HTTP_UNAUTHORIZED -> LoginError.InvalidCredentials

HTTP_NOT_FOUND -> LoginError.UserNotFound

in HTTP_SERVER_ERROR_MIN..HTTP_SERVER_ERROR_MAX -> LoginError.Server

else -> LoginError.Unknown
}
}

/**
* Извлекает backend key из error body:
* - парсит ErrorResponseDto
* - возвращает null, если body пустой или формат неизвестен
*/
private fun HttpException.getBackendErrorKey(): String? {
val errorBody = response()?.errorBody()?.string() ?: return null

return runCatching {
gson.fromJson(errorBody, ErrorResponseDto::class.java).key
}.getOrNull()
}

private companion object {
private const val HTTP_BAD_REQUEST = 400
private const val HTTP_UNAUTHORIZED = 401
private const val HTTP_NOT_FOUND = 404
private const val HTTP_SERVER_ERROR_MIN = 500
private const val HTTP_SERVER_ERROR_MAX = 599

private const val BACKEND_KEY_INVALID_PASSWORD = "auth.user.password.invalid"
private const val BACKEND_KEY_INVALID_CREDENTIALS = "auth.login.invalid_credentials"
private const val BACKEND_KEY_USER_NOT_FOUND = "user.user.id.not_found"
private const val BACKEND_KEY_ACCOUNT_BLOCKED = "user.user.status.blocked"
private const val BACKEND_KEY_TOO_MANY_ATTEMPTS = "auth.throttle.too_many_attempts"
private const val BACKEND_KEY_EMAIL_NOT_CONFIRMED = "auth.user.email.not_confirmed"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ru.yeahub.authentication.impl.login.data.repository.remote

import ru.yeahub.network_api.models.LoginRequestDto
import ru.yeahub.network_api.models.LoginResponseDto

/**
* Контракт remote data source авторизации:
* - login - выполняет сетевой запрос авторизации
*/
interface LoginRemoteDataSourceApi {

suspend fun login(request: LoginRequestDto): LoginResponseDto
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ru.yeahub.authentication.impl.login.data.repository.remote

import ru.yeahub.network_api.NetworkProvider
import ru.yeahub.network_api.models.LoginRequestDto
import ru.yeahub.network_api.models.LoginResponseDto

/**
* Реализация remote data source авторизации:
* - вызывает методы apiService из NetworkProvider
*/
class LoginRemoteDataSourceImpl(
private val apiService: NetworkProvider,
) : LoginRemoteDataSourceApi {

override suspend fun login(request: LoginRequestDto): LoginResponseDto {
return apiService.apiService.login(request)
}
}
Loading
Loading