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β
| HTTP | Path | Method | Signature |
|---|---|---|---|
POST | api/auth/login | login | suspend fun login(@Body request: LoginRequest): LoginResponseDto |
POST | api/auth/2fa/login | login2FA | suspend fun login2FA(@Body request: Login2FARequest): LoginResponseDto |
POST | api/auth/openid/token | loginWithGoogle | suspend fun loginWithGoogle(@Body request: GoogleLoginRequest): LoginResponseDto |
POST | api/auth/openid/token | loginWithFacebook | suspend fun loginWithFacebook(@Body request: FacebookLoginRequest): LoginResponseDto |
DELETE | api/auth/logout | logout | suspend fun logout() |
POST | api/auth/refresh | refreshToken | suspend fun refreshToken(@Body request: RefreshTokenRequest): LoginResponseDto |
logoutuses@HTTP(method = "DELETE", path = "api/auth/logout", hasBody = false)because@DELETEdoes not support body assertions.
Registration & Email Verificationβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
POST | api/auth/register | register | suspend fun register(@Body request: RegisterRequest): RegisterResponseDto |
POST | api/auth/verify-code | verifyCode | suspend fun verifyCode(@Body request: VerifyCodeRequest): VerifyCodeResponseDto |
POST | api/auth/verify | verifyEmailByToken | suspend fun verifyEmailByToken(@Body request: VerifyEmailTokenRequest): VerifyCodeResponseDto |
POST | api/auth/resend-verification | resendVerification | suspend fun resendVerification(@Body request: ResendVerificationRequest) |
Profile & Accountβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
GET | api/auth/profile | getProfile | suspend fun getProfile(): UserDto |
PUT | api/auth/users/{userId} | updateUser | suspend fun updateUser(@Path("userId") userId: Int, @Body request: UpdateUserRequest): UserDto |
DELETE | api/auth/account | deleteAccount | suspend fun deleteAccount() |
POST | api/auth/accept-terms | acceptTerms | suspend fun acceptTerms(): UserDto |
POST | api/auth/accept-terms | acceptTermsWithName | suspend fun acceptTermsWithName(@Body request: AcceptTermsRequest): UserDto |
acceptTermsandacceptTermsWithNameshare the same endpoint; the latter sends an optionalnamein the body.
Password Managementβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
POST | api/auth/set-password | setPassword | suspend fun setPassword(@Body request: SetPasswordRequest) |
POST | api/auth/password/reset | requestPasswordReset | suspend fun requestPasswordReset(@Body request: PasswordResetRequest) |
POST | api/auth/password/verify-code | verifyResetCode | suspend fun verifyResetCode(@Body request: VerifyCodeRequest) |
POST | api/auth/password/update | updatePassword | suspend fun updatePassword(@Body request: UpdatePasswordRequest) |
POST | api/auth/password/resolve-token | resolveResetToken | suspend fun resolveResetToken(@Body request: ResolveTokenRequest): ResolveTokenResponse |
POST | api/auth/change-password | changePassword | suspend fun changePassword(@Body request: ChangePasswordRequest) |
Two-Factor Authenticationβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
POST | api/auth/2fa/setup | setup2FA | suspend fun setup2FA(): TwoFactorSetupDto |
POST | api/auth/2fa/verify | verify2FA | suspend fun verify2FA(@Body request: Verify2FARequest): BackupCodesDto |
POST | api/auth/2fa/disable | disable2FA | suspend fun disable2FA() |
Legal Documentsβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
GET | api/auth/documents/{type}/{language} | getLegalDocument | suspend 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:
| Method | Status code | Exception thrown |
|---|---|---|
login2FA | 401 | Invalid2FACodeException |
refreshToken | 401 | SessionExpiredException |
verifyCode | 400, 401 | Invalid2FACodeException |
verifyEmailByToken | 400 | Invalid2FACodeException |
verifyResetCode | 400 | Invalid2FACodeException |
changePassword | 401 | InvalidCredentialsException |
resetPassword | 400 | Invalid2FACodeException |
resolveResetToken | 400 | Invalid2FACodeException |
verify2FA | 400, 401 | Invalid2FACodeException |
disable2FA | 400, 401 | Invalid2FACodeException |
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.