# 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)

```kotlin
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

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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)

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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:

```kotlin
// 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

```kotlin
// 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**
```kotlin
// 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

1. **Base URL**: Use environment-based configuration
   ```kotlin
   val baseUrl = if (BuildConfig.DEBUG) 
       "http://localhost:3000" 
   else 
       "https://api.yourdomain.com"
   ```

2. **Certificate Pinning**: For production, implement SSL pinning

3. **Error Logging**: Log errors to crash reporting (Firebase Crashlytics, etc.)

4. **Network Timeout**: Set appropriate timeouts for network requests

5. **Token Refresh Strategy**: Consider proactive refresh (refresh before expiration)

---

## Quick Reference

**Endpoint:** `GET /users/me`

**Headers:**
```http
Authorization: Bearer <access_token>
Content-Type: application/json
```

**Success Response (200):**
```json
{
  "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!




