Get User APi

This commit is contained in:
Chandresh Kerkar 2025-11-30 22:47:20 +05:30
parent 22ed0bfeb3
commit 32ff5064e5
6 changed files with 1193 additions and 0 deletions

1
.gitignore vendored
View File

@ -37,3 +37,4 @@ build/
*.tmp
*.temp

View File

@ -0,0 +1,421 @@
# Gemini Prompt: Implement JWT Authentication with Refresh Token Rotation
## Context
I have a partially built Android Kotlin application that needs secure JWT authentication with rotating refresh tokens for persistent login. The authentication service is already built and running at `http://localhost:3000`. The app should keep users logged in using secure token storage and automatic token refresh.
---
## Authentication Service Details
**Base URL:** `http://localhost:3000` (development)
**Authentication Flow:**
1. User requests OTP via `POST /auth/request-otp`
2. User verifies OTP via `POST /auth/verify-otp` → receives `access_token` and `refresh_token`
3. Access token (15 min expiry) is used for authenticated API calls
4. Refresh token (7 days expiry) is used to get new tokens when access token expires
5. Refresh tokens rotate on each use (must save new refresh_token)
**Key Endpoints:**
### 1. Request OTP
```
POST /auth/request-otp
Body: { "phone_number": "+919876543210" }
Response: { "ok": true }
```
### 2. Verify OTP (Login)
```
POST /auth/verify-otp
Body: {
"phone_number": "+919876543210",
"code": "123456",
"device_id": "android-installation-id",
"device_info": {
"platform": "android",
"model": "Samsung SM-M326B",
"os_version": "Android 14",
"app_version": "1.0.0",
"language_code": "en-IN",
"timezone": "Asia/Kolkata"
}
}
Response: {
"user": { "id": "...", "phone_number": "...", "name": null, ... },
"access_token": "eyJhbGc...",
"refresh_token": "eyJhbGc...",
"needs_profile": true,
"is_new_device": true,
"is_new_account": false
}
```
### 3. Refresh Token (Get New Tokens)
```
POST /auth/refresh
Body: { "refresh_token": "eyJhbGc..." }
Response: {
"access_token": "eyJhbGc...",
"refresh_token": "eyJhbGc..." // NEW token - MUST save this
}
```
### 4. Get User Details (Authenticated)
```
GET /users/me
Headers: Authorization: Bearer <access_token>
Response: {
"id": "...",
"phone_number": "+919876543210",
"name": "John Doe",
"user_type": "seller",
"last_login_at": "2024-01-20T14:22:00Z",
"location": { ... },
"locations": [ ... ],
"active_devices_count": 2
}
```
### 5. Logout
```
POST /auth/logout
Body: { "refresh_token": "eyJhbGc..." }
Response: { "ok": true }
```
---
## Implementation Requirements
### 1. Secure Token Storage
**Use EncryptedSharedPreferences (Android Security Library):**
- Store `access_token` and `refresh_token` securely
- Never use plain SharedPreferences
- Never log tokens in console/logs
- Clear tokens on logout or app uninstall
**Implementation:**
```kotlin
// Use androidx.security:security-crypto:1.1.0-alpha06
// Store tokens using EncryptedSharedPreferences with MasterKey
```
### 2. Token Management
**Access Token:**
- Lifetime: 15 minutes
- Used in `Authorization: Bearer <token>` header for all authenticated requests
- Automatically refreshed when expired (401 response)
**Refresh Token:**
- Lifetime: 7 days (configurable)
- Idle timeout: 3 days (if unused for 3 days, becomes invalid)
- **ROTATES on each refresh** - always save the new refresh_token
- Stored securely, never sent in URLs or logs
**Token Refresh Flow:**
```
1. API call returns 401 (Unauthorized)
2. Get refresh_token from secure storage
3. Call POST /auth/refresh with refresh_token
4. Receive new access_token and new refresh_token
5. Save BOTH new tokens securely
6. Retry original API call with new access_token
7. If refresh fails → clear tokens → redirect to login
```
### 3. Auto-Refresh Interceptor/Plugin
**Implement automatic token refresh:**
- Intercept all API requests
- Add `Authorization: Bearer <access_token>` header automatically
- On 401 response:
- Refresh token automatically
- Retry original request
- If refresh fails, clear tokens and redirect to login
**Use either:**
- Ktor: HTTP client interceptor/plugin
- Retrofit: OkHttp interceptor
### 4. Success Page Implementation
**After successful login (OTP verification), show a Success/Home screen that:**
1. **Fetches User Details:**
- Call `GET /users/me` with access_token
- Display: name, phone_number, user_type, last_login_at
- Display location if available (city, state, pincode)
- Handle loading and error states
2. **Persistent Login:**
- Check for stored tokens on app launch
- If tokens exist and valid → auto-login user
- If tokens expired → attempt refresh
- If refresh fails → show login screen
3. **Logout Button:**
- Clear all tokens from secure storage
- Call `POST /auth/logout` with refresh_token
- Navigate back to login screen
- Show confirmation dialog before logout
### 5. Security Requirements
**Critical Security Rules:**
1. ✅ Store tokens only in `EncryptedSharedPreferences`
2. ✅ Never log tokens or sensitive data
3. ✅ Always use HTTPS in production (http://localhost only for dev)
4. ✅ Implement certificate pinning for production
5. ✅ Handle token reuse detection (if refresh returns 401, force re-login)
6. ✅ Clear tokens on logout, app uninstall, or security breach
7. ✅ Validate device_id consistently (use Android ID or Installation ID)
8. ✅ Handle network errors gracefully without exposing tokens
### 6. Error Handling
**Handle these scenarios:**
- **401 Unauthorized** → Refresh token → Retry
- **401 on refresh** → Clear tokens → Redirect to login (session expired)
- **Network errors** → Show user-friendly message, retry option
- **Invalid OTP** → Show error, allow retry
- **Token expired** → Auto-refresh silently (user shouldn't notice)
- **Refresh token expired** → Show "Session expired, please login again"
### 7. Device Information
**Send device info during login:**
- Use Android ID or Firebase Installation ID for `device_id`
- Collect: platform, model, OS version, app version, language, timezone
- Send in `device_info` object during OTP verification
---
## Code Structure Requirements
### Data Models (Kotlinx Serialization)
```kotlin
@Serializable
data class User(...)
@Serializable
data class VerifyOtpResponse(...)
@Serializable
data class RefreshResponse(...)
// Include all necessary models with @SerialName for snake_case JSON
```
### Token Manager
```kotlin
class TokenManager(context: Context) {
fun saveTokens(accessToken: String, refreshToken: String)
fun getAccessToken(): String?
fun getRefreshToken(): String?
fun clearTokens()
}
```
### API Client with Auto-Refresh
```kotlin
class AuthApiClient {
// Automatically adds Authorization header
// Automatically refreshes token on 401
// Handles token rotation
}
```
### ViewModel Pattern
```kotlin
class SuccessViewModel : ViewModel() {
// Fetch user details
// Handle logout
// Observe authentication state
}
```
---
## User Experience Flow
1. **App Launch:**
- Check for stored tokens
- If valid → navigate to Success/Home screen
- If invalid/expired → show Login screen
2. **Login Screen:**
- Enter phone number → Request OTP
- Enter OTP code → Verify OTP
- On success → Save tokens → Navigate to Success screen
3. **Success/Home Screen:**
- Show loading indicator
- Fetch user details from `/users/me`
- Display: Name, Phone, Profile Type, Location, Last Login
- Show logout button
- Handle token refresh silently if needed
4. **Logout:**
- Show confirmation dialog
- Call logout API
- Clear tokens
- Navigate to Login screen
---
## Dependencies Required
```kotlin
// HTTP Client
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")
// Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
```
---
## Testing Checklist
- [ ] Login with OTP and receive tokens
- [ ] Tokens stored securely in EncryptedSharedPreferences
- [ ] Success page displays user details from `/users/me`
- [ ] Access token auto-refreshes when expired (wait 15+ min)
- [ ] Refresh token rotates correctly (save new token)
- [ ] Logout clears tokens and navigates to login
- [ ] App remembers login after restart (if tokens valid)
- [ ] Session expires gracefully after 3 days idle
- [ ] Network errors handled gracefully
- [ ] No tokens logged in console/logs
---
## Specific Implementation Notes
1. **Phone Number Format:**
- Accept user input (can be 10 digits or with +91)
- Server auto-normalizes: `9876543210``+919876543210`
- Always send in E.164 format
2. **Device ID:**
- Use consistent identifier: Android ID or Firebase Installation ID
- Must be 4-128 alphanumeric characters
- Server sanitizes invalid IDs
3. **Token Rotation:**
- **CRITICAL:** After refresh, the new `refresh_token` replaces the old one
- Old refresh_token becomes invalid immediately
- Always save the new refresh_token from refresh response
4. **Base URL Configuration:**
- Development: `http://localhost:3000`
- Production: Use environment-based configuration
- Consider using BuildConfig for different environments
5. **API Response Handling:**
- All error responses: `{ "error": "message" }`
- Success responses vary by endpoint
- Always check HTTP status code before parsing JSON
---
## Security Validation Checklist
Before considering the implementation complete, verify:
- ✅ No tokens in logs/console
- ✅ Tokens only in EncryptedSharedPreferences
- ✅ Automatic token refresh works
- ✅ Token rotation handled correctly
- ✅ Tokens cleared on logout
- ✅ Session expiry handled
- ✅ Network errors don't expose tokens
- ✅ HTTPS for production (when deployed)
---
## Expected Behavior
**On Successful Login:**
1. Save `access_token` and `refresh_token` securely
2. Navigate to Success/Home screen
3. Automatically fetch user details from `/users/me`
4. Display user information
5. Show logout button
**On App Restart (with valid tokens):**
1. Check stored tokens
2. Validate token (or refresh if expired)
3. Auto-navigate to Success/Home screen
4. Fetch and display user details
**On Token Expiration:**
1. Next API call returns 401
2. Automatically refresh token (silently)
3. Retry API call with new token
4. User experience uninterrupted
**On Refresh Token Expiration:**
1. Refresh attempt fails with 401
2. Clear all tokens
3. Show "Session expired" message
4. Navigate to login screen
**On Logout:**
1. Show confirmation dialog
2. Call logout API
3. Clear all tokens from storage
4. Navigate to login screen
---
## Reference Documentation
The complete API documentation and data models are available in:
- `how_to_use_Auth.md` (in your Android project)
- Full API reference with request/response examples
- All data models with Kotlinx Serialization annotations
---
## Deliverables
Please implement:
1. **TokenManager** - Secure token storage using EncryptedSharedPreferences
2. **AuthApiClient** - API client with automatic token refresh and rotation
3. **Success/Home Screen** - Displays user details from `/users/me`
4. **Logout functionality** - With confirmation and proper cleanup
5. **Auto-login on app launch** - Check tokens and auto-login if valid
6. **Error handling** - Graceful handling of all error scenarios
7. **Loading states** - Show loading indicators during API calls
**Code Quality:**
- Use Kotlin best practices
- Follow MVVM architecture pattern
- Use Kotlin Coroutines for async operations
- Proper error handling with Result type
- Clean, maintainable, and secure code
**Security:**
- All tokens encrypted at rest
- No tokens in logs
- Proper token rotation
- Secure network communication
---
This implementation should provide a secure, production-ready authentication system with persistent login capability. The user should be able to login once and remain logged in (until tokens expire or logout) with seamless token refresh happening automatically in the background.

81
GEMINI_PROMPT_CONCISE.md Normal file
View File

@ -0,0 +1,81 @@
# Gemini Prompt: JWT Auth with Refresh Token Rotation - Copy This to Gemini
---
I need you to implement secure JWT authentication with rotating refresh tokens in my Android Kotlin app for persistent login. The auth service runs at `http://localhost:3000`.
## API Endpoints
**Base URL:** `http://localhost:3000`
1. **Request OTP:** `POST /auth/request-otp` → Body: `{ "phone_number": "+919876543210" }`
2. **Verify OTP:** `POST /auth/verify-otp` → Returns: `{ "access_token", "refresh_token", "user", ... }`
3. **Refresh Token:** `POST /auth/refresh` → Body: `{ "refresh_token": "..." }` → Returns new access_token AND new refresh_token (ROTATES)
4. **Get User:** `GET /users/me` → Header: `Authorization: Bearer <access_token>` → Returns user details with location
5. **Logout:** `POST /auth/logout` → Body: `{ "refresh_token": "..." }`
## Critical Requirements
**Token Storage (SECURITY):**
- ✅ Use `EncryptedSharedPreferences` (androidx.security:security-crypto)
- ❌ NEVER use plain SharedPreferences
- ❌ NEVER log tokens in console/logs
- ✅ Clear tokens on logout
**Token Management:**
- Access token: 15 min lifetime, used in `Authorization: Bearer <token>` header
- Refresh token: 7 days lifetime, rotates on each refresh (SAVE NEW TOKEN)
- Auto-refresh on 401: Get new tokens, retry request, if refresh fails → logout
**Success/Home Screen:**
- After login → Navigate to Success screen
- Fetch user details from `GET /users/me` with access_token
- Display: name, phone_number, user_type, last_login_at, location
- Show logout button with confirmation
- Handle loading/error states
**Persistent Login:**
- On app launch: Check stored tokens → If valid, auto-login to Success screen
- If tokens expired: Try refresh → If fails, show login screen
- User should stay logged in until logout or 7 days of inactivity
## Implementation Tasks
1. **TokenManager** - Secure storage using EncryptedSharedPreferences
2. **AuthApiClient** - With auto-refresh interceptor (handles 401, refreshes, retries)
3. **Success/Home Activity/Fragment** - Displays user details from `/users/me`
4. **Logout** - Calls logout API, clears tokens, navigates to login
5. **Auto-login** - Check tokens on app launch
## Code Requirements
- Use Kotlinx Serialization for JSON
- Use Ktor or Retrofit for HTTP client
- Use MVVM architecture
- Use Kotlin Coroutines
- Handle all errors gracefully
- Show loading indicators
## Security Checklist
- ✅ Tokens only in EncryptedSharedPreferences
- ✅ Auto-refresh on token expiration
- ✅ Token rotation handled (save new refresh_token)
- ✅ No tokens in logs
- ✅ Clear tokens on logout
## Dependencies
```kotlin
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")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
```
**IMPORTANT:** Refresh tokens ROTATE - always save the new refresh_token from refresh response. Reference: See `how_to_use_Auth.md` in the project for complete API documentation.
---

642
KOTLIN_INTEGRATION_GUIDE.md Normal file
View File

@ -0,0 +1,642 @@
# 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!

View File

@ -90,3 +90,4 @@ When Twilio is not configured:
This is perfect for local development!

View File

@ -58,6 +58,16 @@
<pre id="updateProfileResult"></pre>
</div>
<div class="section">
<h2>4. Get User Details</h2>
<p>Authenticated GET request to fetch user profile with JWT token</p>
<p>Shows: phone number, name, profile type, location, last login, and all saved locations</p>
<button onclick="getUserDetails()">Get User Details</button>
<pre id="getUserDetailsResult"></pre>
</div>
<script>
let accessToken = null;
let refreshToken = null;
@ -150,6 +160,43 @@
out.textContent = 'Error: ' + err;
}
}
async function getUserDetails() {
const out = document.getElementById('getUserDetailsResult');
if (!accessToken) {
alert('No access_token. Verify OTP first.');
out.textContent = 'Error: No access token. Please verify OTP first.';
return;
}
out.textContent = 'Fetching user details...';
try {
const res = await fetch('/users/me', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken,
}
});
const data = await res.json();
if (res.ok) {
out.textContent = JSON.stringify(data, null, 2);
} else {
out.textContent = `Error ${res.status}: ${JSON.stringify(data, null, 2)}`;
// If token expired, try to refresh
if (res.status === 401) {
alert('Access token expired. Try refreshing the token or verify OTP again.');
}
}
} catch (err) {
out.textContent = 'Error: ' + err;
}
}
</script>
</body>
</html>