mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-06-05 13:52:54 +00:00
API calls now use X-Session-Token.
This commit is contained in:
parent
efa4bab144
commit
9365bc0215
@ -0,0 +1,31 @@
|
||||
package com.github.nacabaro.vbhelper.battle
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* OkHttp interceptor that adds Authorization header to API requests.
|
||||
* Skips adding header for auth endpoints.
|
||||
*/
|
||||
class AuthInterceptor(private val token: String) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
// Skip adding auth header for auth endpoints
|
||||
if (originalRequest.url.encodedPath.startsWith("/api/auth")) {
|
||||
return chain.proceed(originalRequest)
|
||||
}
|
||||
|
||||
// Add authentication header for game endpoints
|
||||
// Use X-Session-Token header (preferred) or Authorization: Bearer
|
||||
val authenticatedRequest = originalRequest.newBuilder()
|
||||
.header("X-Session-Token", token)
|
||||
.build()
|
||||
|
||||
// Debug: Log which header is being used (first few chars of token for security)
|
||||
val tokenPreview = if (token.length > 8) "${token.take(4)}...${token.takeLast(4)}" else "***"
|
||||
println("AuthInterceptor: Adding X-Session-Token header (token: $tokenPreview)")
|
||||
|
||||
return chain.proceed(authenticatedRequest)
|
||||
}
|
||||
}
|
||||
@ -7,5 +7,8 @@ import retrofit2.http.POST
|
||||
interface AuthService {
|
||||
@POST("api/auth/validate")
|
||||
fun validate(@Body request: AuthenticateRequest): Call<AuthenticateResponse>
|
||||
|
||||
@POST("api/auth/login")
|
||||
fun login(@Body request: AuthenticateRequest): Call<AuthenticateResponse>
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ data class UserInfo(
|
||||
data class AuthenticateResponse(
|
||||
val success: Boolean,
|
||||
val message: String? = null,
|
||||
val userInfo: UserInfo? = null
|
||||
val userInfo: UserInfo? = null,
|
||||
val sessionToken: String? = null
|
||||
)
|
||||
|
||||
|
||||
@ -7,18 +7,131 @@ import retrofit2.*
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import com.github.nacabaro.vbhelper.battle.BattleAuthContainer
|
||||
|
||||
class RetrofitHelper {
|
||||
|
||||
/**
|
||||
* Creates an OkHttpClient with authentication interceptor for game endpoints.
|
||||
* Requires a non-null, non-empty token.
|
||||
*/
|
||||
private fun createAuthenticatedClient(token: String): OkHttpClient {
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(AuthInterceptor(token))
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the session token from AuthRepository for API calls.
|
||||
* Falls back to nacatech token if session token is not available (backward compatibility).
|
||||
*/
|
||||
private fun getAuthToken(context: Context): String? {
|
||||
return try {
|
||||
val authContainer = BattleAuthContainer(context)
|
||||
runBlocking {
|
||||
// Prefer session token, fall back to nacatech token for backward compatibility
|
||||
val sessionToken = authContainer.authRepository.sessionToken.first()
|
||||
if (!sessionToken.isNullOrEmpty()) {
|
||||
println("RetrofitHelper: Using sessionToken for API call")
|
||||
sessionToken
|
||||
} else {
|
||||
// Fallback to nacatech token (slower, but works)
|
||||
val nacatechToken = authContainer.authRepository.authToken.first()
|
||||
if (!nacatechToken.isNullOrEmpty()) {
|
||||
println("RetrofitHelper: No sessionToken found, falling back to nacatechToken")
|
||||
}
|
||||
nacatechToken
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("RetrofitHelper: Error getting auth token: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Retrofit instance with authentication for game endpoints.
|
||||
*/
|
||||
private fun createAuthenticatedRetrofit(context: Context): Retrofit? {
|
||||
val token = getAuthToken(context)
|
||||
if (token.isNullOrEmpty()) {
|
||||
println("RetrofitHelper: No auth token available")
|
||||
return null
|
||||
}
|
||||
|
||||
val client = createAuthenticatedClient(token)
|
||||
return Retrofit.Builder()
|
||||
.baseUrl("http://battle.io-void.com:8080/")
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles HTTP error responses (401, 403, 429).
|
||||
* For 401/403, clears authentication state to trigger re-authentication.
|
||||
*/
|
||||
private fun handleErrorResponse(context: Context, response: Response<*>, errorMessage: String) {
|
||||
when (response.code()) {
|
||||
401 -> {
|
||||
println("RetrofitHelper: Authentication failed (401) - token may be expired")
|
||||
clearAuthAndNotify(context, "Authentication failed. Please log in again.")
|
||||
}
|
||||
403 -> {
|
||||
println("RetrofitHelper: Access forbidden (403) - token may be expired or invalid")
|
||||
// 403 could mean expired token, so clear auth state to trigger re-authentication
|
||||
clearAuthAndNotify(context, "Session expired. Please log in again.")
|
||||
}
|
||||
429 -> {
|
||||
println("RetrofitHelper: Rate limit exceeded (429)")
|
||||
Toast.makeText(context, "Too many requests. Please wait a moment.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> {
|
||||
println("RetrofitHelper: API error (${response.code()}): $errorMessage")
|
||||
Toast.makeText(context, "Request failed: ${response.code()}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears authentication state and shows a message.
|
||||
* This will trigger BattlesScreen to detect the auth state change and open the login page.
|
||||
*/
|
||||
private fun clearAuthAndNotify(context: Context, message: String) {
|
||||
try {
|
||||
val authContainer = BattleAuthContainer(context)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
authContainer.authRepository.logout()
|
||||
println("RetrofitHelper: Cleared authentication state due to expired/invalid token")
|
||||
}
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
println("RetrofitHelper: Error clearing auth state: ${e.message}")
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun getOpponents(context: Context, stage: String, callback: (OpponentsDataModel) -> Unit) {
|
||||
//println("RetrofitHelper: Starting API call for stage: $stage")
|
||||
|
||||
try {
|
||||
// Create a Retrofit instance with the base URL and
|
||||
// a GsonConverterFactory for parsing the response.
|
||||
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory(
|
||||
GsonConverterFactory.create()).build()
|
||||
//println("RetrofitHelper: Retrofit instance created")
|
||||
// Create an authenticated Retrofit instance
|
||||
val retrofit = createAuthenticatedRetrofit(context)
|
||||
if (retrofit == null) {
|
||||
println("RetrofitHelper: Cannot create authenticated Retrofit - no token available")
|
||||
Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Create an ApiService instance from the Retrofit instance.
|
||||
val service: OpponentService = retrofit.create<OpponentService>(OpponentService::class.java)
|
||||
@ -47,13 +160,16 @@ class RetrofitHelper {
|
||||
val opponentsList: OpponentsDataModel = response.body() as OpponentsDataModel
|
||||
callback(opponentsList)
|
||||
} else {
|
||||
println("RetrofitHelper: Response not successful - Error: ${response.errorBody()?.string()}")
|
||||
val errorBody = response.errorBody()?.string()
|
||||
println("RetrofitHelper: Response not successful - Error: $errorBody")
|
||||
handleErrorResponse(context, response, errorBody ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
println("RetrofitHelper: Exception in getOpponents: ${e.message}")
|
||||
e.printStackTrace()
|
||||
Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +178,7 @@ class RetrofitHelper {
|
||||
|
||||
// Create a Retrofit instance with the base URL and
|
||||
// a GsonConverterFactory for parsing the response.
|
||||
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory(
|
||||
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://battle.io-void.com:8080/").addConverterFactory(
|
||||
GsonConverterFactory.create()).build()
|
||||
|
||||
// Create an ApiService instance from the Retrofit instance.
|
||||
@ -102,7 +218,7 @@ class RetrofitHelper {
|
||||
|
||||
// Create a Retrofit instance with the base URL and
|
||||
// a GsonConverterFactory for parsing the response.
|
||||
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory(
|
||||
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://battle.io-void.com:8080/").addConverterFactory(
|
||||
GsonConverterFactory.create()).build()
|
||||
|
||||
// Create an ApiService instance from the Retrofit instance.
|
||||
@ -141,10 +257,14 @@ class RetrofitHelper {
|
||||
|
||||
fun getPVPWinner(context: Context, apiStage: Int, playerID: Int, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, callback: (PVPDataModel) -> Unit) {
|
||||
|
||||
// Create a Retrofit instance with the base URL and
|
||||
// a GsonConverterFactory for parsing the response.
|
||||
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory(
|
||||
GsonConverterFactory.create()).build()
|
||||
try {
|
||||
// Create an authenticated Retrofit instance
|
||||
val retrofit = createAuthenticatedRetrofit(context)
|
||||
if (retrofit == null) {
|
||||
println("RetrofitHelper: Cannot create authenticated Retrofit - no token available")
|
||||
Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Create an ApiService instance from the Retrofit instance.
|
||||
val service: PVPService = retrofit.create<PVPService>(PVPService::class.java)
|
||||
@ -160,11 +280,14 @@ class RetrofitHelper {
|
||||
|
||||
override fun onFailure(call: Call<PVPDataModel>, t: Throwable) {
|
||||
// This method is called when the API request fails.
|
||||
println("RetrofitHelper: PVP API call failed: ${t.message}")
|
||||
t.printStackTrace()
|
||||
Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<PVPDataModel>, response: Response<PVPDataModel>) {
|
||||
// This method is called when the API response is received successfully.
|
||||
println("RetrofitHelper: PVP API response received - Code: ${response.code()}")
|
||||
|
||||
if(response.isSuccessful){
|
||||
// If the response is successful, parse the
|
||||
@ -174,9 +297,18 @@ class RetrofitHelper {
|
||||
// Call the callback function with the DataModel
|
||||
// object as a parameter.
|
||||
callback(apiResults)
|
||||
} else {
|
||||
val errorBody = response.errorBody()?.string()
|
||||
println("RetrofitHelper: PVP API response not successful - Code: ${response.code()}, Error: $errorBody")
|
||||
handleErrorResponse(context, response, errorBody ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
println("RetrofitHelper: Exception in getPVPWinner: ${e.message}")
|
||||
e.printStackTrace()
|
||||
Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun authenticate(context: Context, token: String, callback: (AuthenticateResponse) -> Unit) {
|
||||
@ -198,14 +330,15 @@ class RetrofitHelper {
|
||||
.build()
|
||||
|
||||
val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl("http://192.168.0.230:8080/")
|
||||
.baseUrl("http://battle.io-void.com:8080/")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
|
||||
val service: AuthService = retrofit.create<AuthService>(AuthService::class.java)
|
||||
val request = AuthenticateRequest(userToken = token)
|
||||
val call: Call<AuthenticateResponse> = service.validate(request)
|
||||
// Use login endpoint instead of validate to get sessionToken
|
||||
val call: Call<AuthenticateResponse> = service.login(request)
|
||||
|
||||
call.enqueue(object : Callback<AuthenticateResponse> {
|
||||
override fun onFailure(call: Call<AuthenticateResponse>, t: Throwable) {
|
||||
|
||||
@ -72,6 +72,7 @@ import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage
|
||||
import com.github.nacabaro.vbhelper.battle.HitEffectOverlay
|
||||
import com.github.nacabaro.vbhelper.battle.BattleAuthContainer
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
//import kotlin.math.sin
|
||||
@ -805,7 +806,7 @@ fun MiddleBattleView(
|
||||
) {
|
||||
// Enemy HP bar (top)
|
||||
LinearProgressIndicator(
|
||||
progress = battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f),
|
||||
progress = { battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f) },
|
||||
modifier = getLandscapeModifier(),
|
||||
color = Color.Red,
|
||||
trackColor = Color.Gray
|
||||
@ -1017,7 +1018,7 @@ fun MiddleBattleView(
|
||||
) {
|
||||
// Critical bar
|
||||
LinearProgressIndicator(
|
||||
progress = battleSystem.critBarProgress / 100f,
|
||||
progress = { battleSystem.critBarProgress / 100f },
|
||||
modifier = getLandscapeModifier(),
|
||||
color = Color.Yellow,
|
||||
trackColor = Color.Gray
|
||||
@ -1035,7 +1036,7 @@ fun MiddleBattleView(
|
||||
) {
|
||||
// Player HP bar
|
||||
LinearProgressIndicator(
|
||||
progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f),
|
||||
progress = { battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f) },
|
||||
modifier = getLandscapeModifier(),
|
||||
color = Color.Green,
|
||||
trackColor = Color.Gray
|
||||
@ -1215,7 +1216,7 @@ fun PlayerBattleView(
|
||||
) {
|
||||
// Health bar
|
||||
LinearProgressIndicator(
|
||||
progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f),
|
||||
progress = { battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f) },
|
||||
modifier = getLandscapeModifier(),
|
||||
color = Color.Green,
|
||||
trackColor = Color.Gray
|
||||
@ -1401,7 +1402,7 @@ fun EnemyBattleView(
|
||||
) {
|
||||
// Enemy HP bar
|
||||
LinearProgressIndicator(
|
||||
progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f),
|
||||
progress = { battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f) },
|
||||
modifier = getLandscapeModifier(),
|
||||
color = Color.Red,
|
||||
trackColor = Color.Gray
|
||||
@ -1765,11 +1766,20 @@ fun BattlesScreen() {
|
||||
// Exchange token with battle server
|
||||
RetrofitHelper().authenticate(context, token) { response ->
|
||||
if (response.success) {
|
||||
// Token already marked as processed before API call, just extract userId
|
||||
// Extract userId from response
|
||||
// Extract userId and sessionToken from response
|
||||
val extractedUserId = response.userInfo?.userId?.toLongOrNull()
|
||||
val sessionToken = response.sessionToken
|
||||
|
||||
println("BATTLESCREEN: Authentication successful, userId: $extractedUserId, sessionToken: ${if (sessionToken != null) "present" else "missing"}")
|
||||
|
||||
// Store both nacatech token (for re-auth) and sessionToken (for API calls)
|
||||
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
|
||||
battleAuthContainer.authRepository.setAuthenticated(true, token, extractedUserId)
|
||||
battleAuthContainer.authRepository.setAuthenticated(
|
||||
isAuthenticated = true,
|
||||
nacatechToken = token,
|
||||
sessionToken = sessionToken,
|
||||
userId = extractedUserId
|
||||
)
|
||||
}
|
||||
// Update UI state on main thread
|
||||
kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch {
|
||||
@ -1877,16 +1887,23 @@ fun BattlesScreen() {
|
||||
kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch {
|
||||
if (response.success) {
|
||||
val extractedUserId = response.userInfo?.userId?.toLongOrNull()
|
||||
// Update stored userId
|
||||
val sessionToken = response.sessionToken
|
||||
|
||||
// Update stored userId and sessionToken
|
||||
if (extractedUserId != null) {
|
||||
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
|
||||
authRepository.setAuthenticated(true, storedToken, extractedUserId)
|
||||
authRepository.setAuthenticated(
|
||||
isAuthenticated = true,
|
||||
nacatechToken = storedToken,
|
||||
sessionToken = sessionToken,
|
||||
userId = extractedUserId
|
||||
)
|
||||
}
|
||||
}
|
||||
isAuthenticated = true
|
||||
isCheckingAuth = false
|
||||
userId = extractedUserId
|
||||
println("BATTLESCREEN: Got userId from validation: $extractedUserId")
|
||||
println("BATTLESCREEN: Got userId from validation: $extractedUserId, sessionToken: ${if (sessionToken != null) "present" else "missing"}")
|
||||
} else {
|
||||
println("BATTLESCREEN: Token validation failed: ${response.message}")
|
||||
// Check if it's a critical error that requires re-authentication
|
||||
@ -1999,6 +2016,28 @@ fun BattlesScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// Watch auth repository state changes to detect when token is cleared (e.g., expired token)
|
||||
LaunchedEffect(Unit) {
|
||||
battleAuthContainer.authRepository.isAuthenticated.collect { authState ->
|
||||
if (!authState && isAuthenticated) {
|
||||
// Auth state was cleared (e.g., by RetrofitHelper due to expired token)
|
||||
println("BATTLESCREEN: Auth state cleared, triggering re-authentication")
|
||||
isAuthenticated = false
|
||||
isCheckingAuth = false
|
||||
// Open auth URL to get a fresh token
|
||||
val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token="
|
||||
val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))
|
||||
try {
|
||||
context.startActivity(authIntent)
|
||||
println("BATTLESCREEN: Opened auth URL after token expiration: $authUrl")
|
||||
} catch (e: Exception) {
|
||||
println("BATTLESCREEN: Failed to open auth URL: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check intent when authentication state changes
|
||||
// Only process if it's a fresh ACTION_VIEW intent (deep link)
|
||||
LaunchedEffect(isAuthenticated) {
|
||||
|
||||
@ -14,7 +14,8 @@ class AuthRepository(
|
||||
) {
|
||||
private companion object {
|
||||
val IS_AUTHENTICATED = booleanPreferencesKey("is_authenticated")
|
||||
val AUTH_TOKEN = stringPreferencesKey("auth_token")
|
||||
val AUTH_TOKEN = stringPreferencesKey("auth_token") // Nacatech token (for re-authentication)
|
||||
val SESSION_TOKEN = stringPreferencesKey("session_token") // Session token (for API calls)
|
||||
val USER_ID = longPreferencesKey("user_id")
|
||||
}
|
||||
|
||||
@ -28,16 +29,24 @@ class AuthRepository(
|
||||
preferences[AUTH_TOKEN]
|
||||
}
|
||||
|
||||
val sessionToken: Flow<String?> = dataStore.data
|
||||
.map { preferences ->
|
||||
preferences[SESSION_TOKEN]
|
||||
}
|
||||
|
||||
val userId: Flow<Long?> = dataStore.data
|
||||
.map { preferences ->
|
||||
preferences[USER_ID]
|
||||
}
|
||||
|
||||
suspend fun setAuthenticated(isAuthenticated: Boolean, token: String? = null, userId: Long? = null) {
|
||||
suspend fun setAuthenticated(isAuthenticated: Boolean, nacatechToken: String? = null, sessionToken: String? = null, userId: Long? = null) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[IS_AUTHENTICATED] = isAuthenticated
|
||||
if (token != null) {
|
||||
preferences[AUTH_TOKEN] = token
|
||||
if (nacatechToken != null) {
|
||||
preferences[AUTH_TOKEN] = nacatechToken
|
||||
}
|
||||
if (sessionToken != null) {
|
||||
preferences[SESSION_TOKEN] = sessionToken
|
||||
}
|
||||
if (userId != null) {
|
||||
preferences[USER_ID] = userId
|
||||
@ -49,6 +58,7 @@ class AuthRepository(
|
||||
dataStore.edit { preferences ->
|
||||
preferences[IS_AUTHENTICATED] = false
|
||||
preferences.remove(AUTH_TOKEN)
|
||||
preferences.remove(SESSION_TOKEN)
|
||||
preferences.remove(USER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user