Skip to main content

Auth Service β€” Data Layer

Authentication data layer: Retrofit API definitions, DTOs, repository implementation, and mappers.


AuthApi​

com.visla.vislagps.data.remote.api.AuthApi

Retrofit interface with suspend endpoints for all authentication operations. Base path: api/auth/.

Login & Session​

HTTPPathMethodSignature
POSTapi/auth/loginloginsuspend fun login(@Body request: LoginRequest): LoginResponseDto
POSTapi/auth/2fa/loginlogin2FAsuspend fun login2FA(@Body request: Login2FARequest): LoginResponseDto
POSTapi/auth/openid/tokenloginWithGooglesuspend fun loginWithGoogle(@Body request: GoogleLoginRequest): LoginResponseDto
POSTapi/auth/openid/tokenloginWithFacebooksuspend fun loginWithFacebook(@Body request: FacebookLoginRequest): LoginResponseDto
DELETEapi/auth/logoutlogoutsuspend fun logout()
POSTapi/auth/refreshrefreshTokensuspend fun refreshToken(@Body request: RefreshTokenRequest): LoginResponseDto

logout uses @HTTP(method = "DELETE", path = "api/auth/logout", hasBody = false) because @DELETE does not support body assertions.

Registration & Email Verification​

HTTPPathMethodSignature
POSTapi/auth/registerregistersuspend fun register(@Body request: RegisterRequest): RegisterResponseDto
POSTapi/auth/verify-codeverifyCodesuspend fun verifyCode(@Body request: VerifyCodeRequest): VerifyCodeResponseDto
POSTapi/auth/verifyverifyEmailByTokensuspend fun verifyEmailByToken(@Body request: VerifyEmailTokenRequest): VerifyCodeResponseDto
POSTapi/auth/resend-verificationresendVerificationsuspend fun resendVerification(@Body request: ResendVerificationRequest)

Profile & Account​

HTTPPathMethodSignature
GETapi/auth/profilegetProfilesuspend fun getProfile(): UserDto
PUTapi/auth/users/{userId}updateUsersuspend fun updateUser(@Path("userId") userId: Int, @Body request: UpdateUserRequest): UserDto
DELETEapi/auth/accountdeleteAccountsuspend fun deleteAccount()
POSTapi/auth/accept-termsacceptTermssuspend fun acceptTerms(): UserDto
POSTapi/auth/accept-termsacceptTermsWithNamesuspend fun acceptTermsWithName(@Body request: AcceptTermsRequest): UserDto

acceptTerms and acceptTermsWithName share the same endpoint; the latter sends an optional name in the body.

Password Management​

HTTPPathMethodSignature
POSTapi/auth/set-passwordsetPasswordsuspend fun setPassword(@Body request: SetPasswordRequest)
POSTapi/auth/password/resetrequestPasswordResetsuspend fun requestPasswordReset(@Body request: PasswordResetRequest)
POSTapi/auth/password/verify-codeverifyResetCodesuspend fun verifyResetCode(@Body request: VerifyCodeRequest)
POSTapi/auth/password/updateupdatePasswordsuspend fun updatePassword(@Body request: UpdatePasswordRequest)
POSTapi/auth/password/resolve-tokenresolveResetTokensuspend fun resolveResetToken(@Body request: ResolveTokenRequest): ResolveTokenResponse
POSTapi/auth/change-passwordchangePasswordsuspend fun changePassword(@Body request: ChangePasswordRequest)

Two-Factor Authentication​

HTTPPathMethodSignature
POSTapi/auth/2fa/setupsetup2FAsuspend fun setup2FA(): TwoFactorSetupDto
POSTapi/auth/2fa/verifyverify2FAsuspend fun verify2FA(@Body request: Verify2FARequest): BackupCodesDto
POSTapi/auth/2fa/disabledisable2FAsuspend fun disable2FA()
HTTPPathMethodSignature
GETapi/auth/documents/{type}/{language}getLegalDocumentsuspend fun getLegalDocument(@Path("type") type: String, @Path("language") language: String): LegalDocumentDto

AuthApiSync​

com.visla.vislagps.data.remote.api.AuthApiSync

Synchronous variant of the auth API. Returns Retrofit Call<T> instead of using suspend.

interface AuthApiSync {
@POST("api/auth/refresh")
fun refreshToken(@Body request: RefreshTokenRequest): Call<LoginResponseDto>
}

Why it exists​

OkHttp's Authenticator runs on a dedicated OkHttp thread. Using runBlocking with a suspend function inside the authenticator can deadlock when all OkHttp dispatcher threads are blocked waiting for the same token refresh network call. AuthApiSync avoids this by calling Call.execute() synchronously on the authenticator thread, keeping the coroutine dispatcher free.


Auth DTOs​

com.visla.vislagps.data.remote.dto.AuthDtos

All request and response models used by AuthApi. Fields annotated with @SerializedName are noted where the JSON key differs from the Kotlin property.

Request DTOs​

data class LoginRequest(val email: String, val password: String)

data class Login2FARequest(
@SerializedName("temp_token") val tempToken: String,
val code: String
)

data class RefreshTokenRequest(val refreshToken: String)

data class RegisterRequest(
val name: String,
val email: String,
val password: String,
val termsPrivacyAccepted: Boolean = true,
val language: String = java.util.Locale.getDefault().language
)

data class VerifyCodeRequest(val email: String, val code: String)

data class ResendVerificationRequest(val email: String)

data class UpdateUserRequest(
val name: String? = null,
val phone: String? = null,
val timezone: String? = null,
val language: String? = null,
val deviceReadonly: Boolean? = null,
val limitCommands: Boolean? = null
)

data class SetPasswordRequest(val password: String)

data class PasswordResetRequest(val email: String)

data class UpdatePasswordRequest(val email: String, val code: String, val password: String)

data class ResolveTokenRequest(val token: String)

data class VerifyEmailTokenRequest(val token: String)

data class ChangePasswordRequest(
@SerializedName("current_password") val currentPassword: String,
@SerializedName("new_password") val newPassword: String
)

data class Verify2FARequest(val code: String)

data class GoogleLoginRequest(
val provider: String = "google",
@SerializedName("id_token") val idToken: String
)

data class FacebookLoginRequest(
val provider: String = "facebook",
@SerializedName("id_token") val idToken: String
)

data class AcceptTermsRequest(
val name: String? = null,
@SerializedName("terms_accepted") val termsAccepted: Boolean = true
)

Response DTOs​

data class LoginResponseDto(
val user: UserDto? = null,
val accessToken: String? = null,
val refreshToken: String? = null,
val expiresIn: Int = 3600,
@SerializedName("require_2fa") val require2fa: Boolean? = null,
@SerializedName("temp_token") val tempToken: String? = null,
val type: String? = null
)

data class RegisterResponseDto(val message: String? = null, val email: String? = null)

data class VerifyCodeResponseDto(
val message: String? = null,
val accessToken: String? = null,
val user: UserDto? = null
)

data class UserDto(
val id: Int,
val email: String? = null,
val name: String? = null,
val role: String? = null,
val termsAccepted: Boolean? = null,
val hasPassword: Boolean? = null,
val emailVerified: Boolean? = null,
val twoFactorEnabled: Boolean? = null,
val timezone: String? = null,
val language: String? = null
)

data class TwoFactorSetupDto(
val secret: String,
@SerializedName("otpauth_url") val otpauthUrl: String
)

data class BackupCodesDto(
@SerializedName("backup_codes") val backupCodes: List<String>
)

data class ResolveTokenResponse(val email: String, val code: String)

data class LegalDocumentDto(
val type: String,
val language: String,
val content: String,
val version: String? = null
)

AuthRepository​

com.visla.vislagps.domain.repositories.AuthRepository

Domain-level interface. All methods are suspend and return domain entities β€” no DTOs leak into the domain layer.

interface AuthRepository {
suspend fun login(email: String, password: String): LoginResult
suspend fun login2FA(tempToken: String, code: String): LoginResult
suspend fun logout()
suspend fun loginWithGoogle(idToken: String): LoginResult
suspend fun loginWithFacebook(accessToken: String): LoginResult
suspend fun acceptTermsWithName(name: String): User
suspend fun refreshToken(refreshToken: String): LoginResult
suspend fun register(name: String, email: String, password: String, acceptTerms: Boolean): RegisterResult
suspend fun verifyCode(email: String, code: String): VerifyEmailResult
suspend fun verifyEmailByToken(token: String): VerifyEmailResult
suspend fun resendVerification(email: String)
suspend fun getProfile(): User
suspend fun updateUser(
userId: Int,
name: String? = null,
phone: String? = null,
timezone: String? = null,
language: String? = null
): User
suspend fun deleteAccount()
suspend fun acceptTerms(): User
suspend fun setPassword(password: String)
suspend fun requestPasswordReset(email: String)
suspend fun verifyResetCode(email: String, code: String)
suspend fun changePassword(currentPassword: String, newPassword: String)
suspend fun resetPassword(email: String, code: String, newPassword: String)
suspend fun resolveResetToken(token: String): ResetTokenResult
suspend fun setup2FA(): TwoFactorSetup
suspend fun verify2FA(code: String): List<String>
suspend fun disable2FA(code: String)
suspend fun getLegalDocument(type: String, language: String): LegalDocument
}

data class TwoFactorSetup(val secret: String, val otpauthUrl: String)
data class ResetTokenResult(val email: String, val code: String)

TwoFactorSetup and ResetTokenResult are declared alongside the interface in the same file.


AuthRepositoryImpl​

com.visla.vislagps.data.repositories.AuthRepositoryImpl

@Singleton implementation injected via Hilt.

Constructor​

@Singleton
class AuthRepositoryImpl @Inject constructor(
private val authApi: AuthApi,
private val userMapper: UserMapper,
private val deviceDataStore: DeviceDataStore,
private val geofenceDataStore: GeofenceDataStore,
private val notificationDataStore: NotificationDataStore,
private val inviteDataStore: InviteDataStore,
private val sharingDataStore: SharingDataStore,
private val realTimeDataBridge: RealTimeDataBridge
) : AuthRepository

The data-store and bridge dependencies are used during logout() to clear all cached data:

override suspend fun logout() {
realTimeDataBridge.stop()
deviceDataStore.clear()
geofenceDataStore.clear()
notificationDataStore.clear()
inviteDataStore.clear()
sharingDataStore.clear()
authApi.logout()
}

Key implementation patterns​

2FA detection on login β€” login, loginWithGoogle, and loginWithFacebook inspect the response for require_2fa == true and throw TwoFactorRequiredException with the temp token and type (defaults to "totp").

disable2FA β€” the repository interface accepts a code parameter, but the backend endpoint requires no code; the API call is authApi.disable2FA() with no body.

resetPassword β€” maps to authApi.updatePassword(UpdatePasswordRequest(...)).

Error mapping​

Every method wraps API calls in try/catch for HttpException and IOException. The private mapAuthException function converts HTTP status codes to domain exceptions:

private fun mapAuthException(e: HttpException): DomainException {
val serverMessage = try {
e.response()?.errorBody()?.string()?.let { body ->
org.json.JSONObject(body).optString("detail", "")
}
} catch (_: Exception) { null }

return when (e.code()) {
HttpStatusCode.UNAUTHORIZED -> InvalidCredentialsException()
HttpStatusCode.FORBIDDEN -> AuthenticationException(serverMessage?.takeIf { it.isNotBlank() } ?: "Access denied")
HttpStatusCode.UNPROCESSABLE_ENTITY -> ValidationException("Validation error")
HttpStatusCode.TOO_MANY_REQUESTS -> RateLimitException()
else -> NetworkException("HTTP error: ${e.code()}", e)
}
}

Some methods apply additional status-code checks before calling mapAuthException:

MethodStatus codeException thrown
login2FA401Invalid2FACodeException
refreshToken401SessionExpiredException
verifyCode400, 401Invalid2FACodeException
verifyEmailByToken400Invalid2FACodeException
verifyResetCode400Invalid2FACodeException
changePassword401InvalidCredentialsException
resetPassword400Invalid2FACodeException
resolveResetToken400Invalid2FACodeException
verify2FA400, 401Invalid2FACodeException
disable2FA400, 401Invalid2FACodeException

All IOException instances are wrapped in NetworkException with a context message.


UserMapper​

com.visla.vislagps.data.mappers.UserMapper

@Singleton class (injected via @Inject constructor()) that converts DTOs to domain entities. No external dependencies.

Methods​

fun toDomain(dto: UserDto): User

Maps UserDto β†’ User. Applies defaults for nullable DTO fields: email and name default to "", role is parsed via UserRole.fromString(dto.role), booleans default to false or true as appropriate, and timezone/language pass through as nullable.

fun toLoginResult(dto: LoginResponseDto): LoginResult

Maps LoginResponseDto β†’ LoginResult. If dto.user is null, creates a fallback User with id = 0 and empty/default fields. accessToken defaults to "", refreshToken is nullable, expiresIn passes through directly.

fun toRegisterResult(dto: RegisterResponseDto): RegisterResult

Maps RegisterResponseDto β†’ RegisterResult. Both message and email default to "Success" / "" when null.

fun toVerifyEmailResult(dto: VerifyCodeResponseDto): VerifyEmailResult

Maps VerifyCodeResponseDto β†’ VerifyEmailResult. message defaults to "Success", accessToken and user are nullable pass-through (user is mapped via toDomain when present).

fun toTwoFactorSetup(dto: TwoFactorSetupDto): TwoFactorSetup

Maps TwoFactorSetupDto β†’ TwoFactorSetup. Direct field copy of secret and otpauthUrl.