package com.syncflow.ui.auth import android.content.Context import android.content.Intent import android.net.Uri import android.util.Base64 import androidx.browser.customtabs.CustomTabsIntent import com.syncflow.data.security.CredentialStore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request import java.security.MessageDigest import java.security.SecureRandom const val OAUTH_REDIRECT_ACTION = "com.syncflow.OAUTH_RESULT" const val OAUTH_EXTRA_TOKEN = "token" const val OAUTH_EXTRA_EMAIL = "email" const val OAUTH_EXTRA_PROVIDER = "provider" private val client = OkHttpClient() private val random = SecureRandom() private fun generateVerifier(): String { val bytes = ByteArray(32) random.nextBytes(bytes) return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) } private fun generateChallenge(verifier: String): String { val digest = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII)) return Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) } fun launchDropboxOAuth(context: Context, credentialStore: CredentialStore, appKey: String) { val verifier = generateVerifier() val state = generateVerifier() credentialStore.savePkceVerifier("dropbox", verifier) credentialStore.savePkceVerifier("dropbox_state", state) val challenge = generateChallenge(verifier) val url = "https://www.dropbox.com/oauth2/authorize" + "?client_id=$appKey" + "&response_type=code" + "&redirect_uri=syncflow%3A%2F%2Foauth%2Fdropbox" + "&code_challenge=$challenge" + "&code_challenge_method=S256" + "&token_access_type=offline" + "&state=$state" openCustomTab(context, url) } fun launchOneDriveOAuth(context: Context, credentialStore: CredentialStore, clientId: String) { val verifier = generateVerifier() val state = generateVerifier() credentialStore.savePkceVerifier("onedrive", verifier) credentialStore.savePkceVerifier("onedrive_state", state) val challenge = generateChallenge(verifier) val scopes = "Files.ReadWrite+User.Read+offline_access" val url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" + "?client_id=$clientId" + "&response_type=code" + "&redirect_uri=syncflow%3A%2F%2Foauth%2Fonedrive" + "&scope=$scopes" + "&code_challenge=$challenge" + "&code_challenge_method=S256" + "&state=$state" openCustomTab(context, url) } private fun openCustomTab(context: Context, url: String) { try { CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(url)) } catch (_: Exception) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }) } } suspend fun exchangeDropboxCode( credentialStore: CredentialStore, code: String, appKey: String, ): Pair? = withContext(Dispatchers.IO) { val verifier = credentialStore.getPkceVerifier("dropbox") ?: return@withContext null credentialStore.removePkceVerifier("dropbox") val body = FormBody.Builder() .add("code", code) .add("grant_type", "authorization_code") .add("client_id", appKey) .add("redirect_uri", "syncflow://oauth/dropbox") .add("code_verifier", verifier) .build() val req = Request.Builder().url("https://api.dropboxapi.com/oauth2/token").post(body).build() val resp = runCatching { client.newCall(req).execute() }.getOrNull() ?: return@withContext null val text = resp.body?.string() ?: return@withContext null val json = runCatching { Json.parseToJsonElement(text).jsonObject }.getOrNull() ?: return@withContext null val token = json["access_token"]?.jsonPrimitive?.content ?: return@withContext null val email = json["account_id"]?.jsonPrimitive?.content ?: "" Pair(token, email) } suspend fun exchangeOneDriveCode( credentialStore: CredentialStore, code: String, clientId: String, ): Pair? = withContext(Dispatchers.IO) { val verifier = credentialStore.getPkceVerifier("onedrive") ?: return@withContext null credentialStore.removePkceVerifier("onedrive") val body = FormBody.Builder() .add("code", code) .add("grant_type", "authorization_code") .add("client_id", clientId) .add("redirect_uri", "syncflow://oauth/onedrive") .add("code_verifier", verifier) .add("scope", "Files.ReadWrite User.Read offline_access") .build() val req = Request.Builder().url("https://login.microsoftonline.com/common/oauth2/v2.0/token").post(body).build() val resp = runCatching { client.newCall(req).execute() }.getOrNull() ?: return@withContext null val text = resp.body?.string() ?: return@withContext null val json = runCatching { Json.parseToJsonElement(text).jsonObject }.getOrNull() ?: return@withContext null val token = json["access_token"]?.jsonPrimitive?.content ?: return@withContext null Pair(token, "") }