18 KiB
How to Use /users/me Endpoint in Kotlin Application
Complete guide for integrating the authenticated /users/me endpoint in your Kotlin/Android application.
Overview
Endpoint: GET http://localhost:3000/users/me (or your production URL)
Authentication Required: Yes (JWT Bearer Token)
What it returns:
- User details (phone, name, profile type)
- Last login time
- Location information (primary + all saved locations)
- Active devices count
Complete Flow
Step 1: User Login (Get Tokens)
POST /auth/verify-otp
→ Returns: { access_token, refresh_token, user, ... }
Step 2: Store Tokens Securely
Save access_token and refresh_token in EncryptedSharedPreferences
Step 3: Make Authenticated Request
GET /users/me
Header: Authorization: Bearer <access_token>
→ Returns: { id, phone_number, name, location, ... }
Step 4: Handle Token Expiration
If 401 error → Use refresh_token to get new tokens
If refresh fails → Redirect to login
Kotlin Implementation
1. Add Dependencies (build.gradle.kts)
dependencies {
// Ktor for HTTP requests
implementation("io.ktor:ktor-client-android:2.3.5")
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
// Secure storage
implementation("androidx.security:security-crypto:1.1.0-alpha06")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
2. Data Models
// models/User.kt
package com.farm.auth.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Coordinates(
val latitude: Double,
val longitude: Double
)
@Serializable
data class Location(
val id: String,
val country: String?,
val state: String?,
val district: String?,
@SerialName("city_village") val cityVillage: String?,
val pincode: String?,
val coordinates: Coordinates?,
@SerialName("location_type") val locationType: String?,
@SerialName("is_saved_address") val isSavedAddress: Boolean,
@SerialName("source_type") val sourceType: String?,
@SerialName("source_confidence") val sourceConfidence: String?,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String
)
@Serializable
data class User(
val id: String,
@SerialName("phone_number") val phoneNumber: String,
val name: String?,
val role: String,
@SerialName("user_type") val userType: String?,
@SerialName("avatar_url") val avatarUrl: String? = null,
val language: String? = null,
val timezone: String? = null,
@SerialName("created_at") val createdAt: String? = null,
@SerialName("last_login_at") val lastLoginAt: String? = null,
@SerialName("active_devices_count") val activeDevicesCount: Int? = null,
val location: Location? = null,
val locations: List<Location> = emptyList()
)
@Serializable
data class ErrorResponse(val error: String)
3. Secure Token Storage
// storage/TokenManager.kt
package com.farm.auth.storage
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class TokenManager(private val context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val prefs: SharedPreferences = EncryptedSharedPreferences.create(
context,
"auth_tokens",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveTokens(accessToken: String, refreshToken: String) {
prefs.edit().apply {
putString("access_token", accessToken)
putString("refresh_token", refreshToken)
apply()
}
}
fun getAccessToken(): String? = prefs.getString("access_token", null)
fun getRefreshToken(): String? = prefs.getString("refresh_token", null)
fun clearTokens() {
prefs.edit().clear().apply()
}
fun hasTokens(): Boolean {
return getAccessToken() != null && getRefreshToken() != null
}
}
4. API Client with Auto-Refresh
// network/AuthApiClient.kt
package com.farm.auth.network
import com.farm.auth.models.*
import com.farm.auth.storage.TokenManager
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.defaultrequest.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.json.Json
class AuthApiClient(
private val baseUrl: String,
private val tokenManager: TokenManager
) {
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
}
private val client = HttpClient(Android) {
install(ContentNegotiation) {
json(json)
}
install(DefaultRequest) {
url(baseUrl)
contentType(ContentType.Application.Json)
}
// Add auth token to all requests automatically
engine {
addInterceptor { request ->
val token = tokenManager.getAccessToken()
if (token != null && !request.url.encodedPath.contains("/auth/")) {
request.headers.append("Authorization", "Bearer $token")
}
request
}
}
}
private val _currentUser = MutableStateFlow<User?>(null)
val currentUser: StateFlow<User?> = _currentUser.asStateFlow()
/**
* Refresh access token using refresh token
*/
suspend fun refreshTokens(): Result<Pair<String, String>> {
val refreshToken = tokenManager.getRefreshToken()
?: return Result.failure(Exception("No refresh token available"))
return try {
val response = client.post("/auth/refresh") {
setBody(mapOf("refresh_token" to refreshToken))
}
if (response.status.isSuccess()) {
val data: RefreshResponse = response.body()
tokenManager.saveTokens(data.accessToken, data.refreshToken)
Result.success(data.accessToken to data.refreshToken)
} else {
val error: ErrorResponse = response.body()
Result.failure(Exception(error.error))
}
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Get current user details from /users/me endpoint
* Automatically handles token refresh on 401
*/
suspend fun getCurrentUser(): Result<User> {
return try {
val response = client.get("/users/me")
if (response.status.isSuccess()) {
val user: User = response.body()
_currentUser.value = user
Result.success(user)
} else if (response.status == HttpStatusCode.Unauthorized) {
// Token expired, try to refresh
refreshTokens().fold(
onSuccess = { (newAccessToken, _) ->
// Retry with new token
val retryResponse = client.get("/users/me") {
header("Authorization", "Bearer $newAccessToken")
}
if (retryResponse.status.isSuccess()) {
val user: User = retryResponse.body()
_currentUser.value = user
Result.success(user)
} else {
// Refresh worked but retry failed - force re-login
tokenManager.clearTokens()
_currentUser.value = null
Result.failure(Exception("Authentication failed. Please login again."))
}
},
onFailure = { error ->
// Refresh failed - force re-login
tokenManager.clearTokens()
_currentUser.value = null
Result.failure(Exception("Session expired. Please login again."))
}
)
} else {
val error: ErrorResponse = response.body()
Result.failure(Exception(error.error))
}
} catch (e: Exception) {
Result.failure(e)
}
}
@Serializable
private data class RefreshResponse(
@SerialName("access_token") val accessToken: String,
@SerialName("refresh_token") val refreshToken: String
)
}
5. Repository Pattern (Optional but Recommended)
// repository/UserRepository.kt
package com.farm.auth.repository
import com.farm.auth.models.User
import com.farm.auth.network.AuthApiClient
import kotlinx.coroutines.flow.StateFlow
class UserRepository(private val apiClient: AuthApiClient) {
val currentUser: StateFlow<User?> = apiClient.currentUser
suspend fun getUserDetails(): Result<User> {
return apiClient.getCurrentUser()
}
suspend fun refreshUserData(): Result<User> {
return apiClient.getCurrentUser()
}
}
6. ViewModel Usage
// ui/UserProfileViewModel.kt
package com.farm.auth.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.farm.auth.models.User
import com.farm.auth.repository.UserRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class UserProfileViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UserProfileUiState>(UserProfileUiState.Loading)
val uiState: StateFlow<UserProfileUiState> = _uiState.asStateFlow()
init {
loadUserProfile()
}
fun loadUserProfile() {
viewModelScope.launch {
_uiState.value = UserProfileUiState.Loading
userRepository.getUserDetails()
.fold(
onSuccess = { user ->
_uiState.value = UserProfileUiState.Success(user)
},
onFailure = { error ->
_uiState.value = UserProfileUiState.Error(error.message ?: "Failed to load profile")
}
)
}
}
fun refreshProfile() {
loadUserProfile()
}
}
sealed class UserProfileUiState {
object Loading : UserProfileUiState()
data class Success(val user: User) : UserProfileUiState()
data class Error(val message: String) : UserProfileUiState()
}
7. Activity/Fragment Usage
// ui/UserProfileActivity.kt
package com.farm.auth.ui
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.farm.auth.network.AuthApiClient
import com.farm.auth.repository.UserRepository
import com.farm.auth.storage.TokenManager
import kotlinx.coroutines.launch
class UserProfileActivity : AppCompatActivity() {
private val viewModel: UserProfileViewModel by viewModels {
val tokenManager = TokenManager(this)
val apiClient = AuthApiClient("http://localhost:3000", tokenManager)
val repository = UserRepository(apiClient)
UserProfileViewModelFactory(repository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_profile)
// Observe UI state
lifecycleScope.launch {
viewModel.uiState.collect { state ->
when (state) {
is UserProfileUiState.Loading -> {
// Show loading indicator
showLoading()
}
is UserProfileUiState.Success -> {
// Display user data
displayUserData(state.user)
hideLoading()
}
is UserProfileUiState.Error -> {
// Show error message
showError(state.message)
hideLoading()
}
}
}
}
}
private fun displayUserData(user: User) {
// Update UI with user data
findViewById<TextView>(R.id.tvName).text = user.name ?: "Not set"
findViewById<TextView>(R.id.tvPhone).text = user.phoneNumber
findViewById<TextView>(R.id.tvProfileType).text = user.userType ?: "Not set"
findViewById<TextView>(R.id.tvLastLogin).text = user.lastLoginAt ?: "Never"
// Display location
user.location?.let { location ->
findViewById<TextView>(R.id.tvLocation).text = buildString {
append(location.cityVillage ?: "")
if (location.district != null) append(", ${location.district}")
if (location.state != null) append(", ${location.state}")
if (location.pincode != null) append(" - ${location.pincode}")
}
} ?: run {
findViewById<TextView>(R.id.tvLocation).text = "No location saved"
}
}
}
Simple Example (Without Repository)
If you want a simpler approach without repository pattern:
// Simple usage in Activity
class MainActivity : AppCompatActivity() {
private lateinit var tokenManager: TokenManager
private lateinit var apiClient: AuthApiClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
tokenManager = TokenManager(this)
apiClient = AuthApiClient("http://localhost:3000", tokenManager)
// Load user profile
loadUserProfile()
}
private fun loadUserProfile() {
lifecycleScope.launch {
apiClient.getCurrentUser()
.onSuccess { user ->
// Use user data
Log.d("User", "Name: ${user.name}")
Log.d("User", "Phone: ${user.phoneNumber}")
Log.d("User", "Location: ${user.location?.cityVillage}")
}
.onFailure { error ->
Log.e("Error", "Failed: ${error.message}")
// Handle error - maybe redirect to login
}
}
}
}
Complete Flow Example
// Complete authentication and profile loading flow
class AuthFlow {
suspend fun loginAndLoadProfile(
phoneNumber: String,
otpCode: String
): Result<User> {
// 1. Login and get tokens
val loginResult = login(phoneNumber, otpCode)
return loginResult.fold(
onSuccess = { tokens ->
// 2. Save tokens
tokenManager.saveTokens(tokens.accessToken, tokens.refreshToken)
// 3. Get user profile
apiClient.getCurrentUser()
},
onFailure = { error ->
Result.failure(error)
}
)
}
}
Key Points
1. Authentication Header Format
Authorization: Bearer <access_token>
2. Token Storage
- ✅ Use
EncryptedSharedPreferences(secure) - ❌ Don't use plain
SharedPreferences - ❌ Don't log tokens in console/logs
3. Token Refresh Flow
401 Error → Refresh Token → Retry Request
If refresh fails → Clear tokens → Redirect to login
4. Base URL Configuration
// Development
val baseUrl = "http://localhost:3000"
// Production (change based on your deployment)
val baseUrl = "https://your-domain.com"
5. Error Handling
- 401 Unauthorized: Token expired → Try refresh
- 404 Not Found: User doesn't exist
- 500 Server Error: Server issue
Testing Checklist
- Login successfully and get tokens
- Store tokens securely
- Fetch user profile with valid token
- Handle token expiration (wait 15+ min, then retry)
- Handle refresh token rotation
- Handle refresh token expiration (force re-login)
- Handle network errors gracefully
- Display user data correctly
- Display location data if available
Production Considerations
-
Base URL: Use environment-based configuration
val baseUrl = if (BuildConfig.DEBUG) "http://localhost:3000" else "https://api.yourdomain.com" -
Certificate Pinning: For production, implement SSL pinning
-
Error Logging: Log errors to crash reporting (Firebase Crashlytics, etc.)
-
Network Timeout: Set appropriate timeouts for network requests
-
Token Refresh Strategy: Consider proactive refresh (refresh before expiration)
Quick Reference
Endpoint: GET /users/me
Headers:
Authorization: Bearer <access_token>
Content-Type: application/json
Success Response (200):
{
"id": "...",
"phone_number": "+919876543210",
"name": "John Doe",
"user_type": "seller",
"last_login_at": "2024-01-20T14:22:00Z",
"location": { ... },
"locations": [ ... ]
}
Error Responses:
- 401: Token expired/invalid → Refresh token
- 404: User not found
- 500: Server error
This guide provides everything you need to integrate the /users/me endpoint in your Kotlin application!