feat: add platform OAuth implementations (Android, iOS, Desktop)
- AndroidOAuthClient: ACTION_VIEW + OAuthCallbackActivity (myice://callback) - IosOAuthClient: ASWebAuthenticationSession with presentation context provider - DesktopOAuthClient: local HTTP server + system browser - AndroidManifest: register OAuthCallbackActivity intent filter - Align Kotlin to 2.3.20 (required by Compose MP 1.11.1 klibs) and compileSdk to 36 (required by activity-compose 1.13.0); fix missing Text import and stray brace in placeholder iOS/Android entry points
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package ch.parano.myice.auth
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import java.awt.Desktop
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.PrintWriter
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class DesktopOAuthClient : OAuthClient {
|
||||
|
||||
private var serverPort: Int = 0
|
||||
|
||||
override fun redirectUri(): String = "http://localhost:$serverPort/callback"
|
||||
|
||||
override suspend fun authenticate(loginUrl: String): String? {
|
||||
val server = ServerSocket()
|
||||
server.bind(InetSocketAddress(0))
|
||||
serverPort = server.localPort
|
||||
|
||||
val redirectUri = "http://localhost:$serverPort/callback"
|
||||
val fullLoginUrl = loginUrl + "&redirect_uri=" + URLEncoder.encode(redirectUri, "UTF-8")
|
||||
|
||||
val deferred = CompletableDeferred<String?>()
|
||||
|
||||
thread {
|
||||
try {
|
||||
while (true) {
|
||||
val socket = server.accept()
|
||||
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
||||
val writer = PrintWriter(socket.getOutputStream())
|
||||
|
||||
val requestLine = reader.readLine() ?: continue
|
||||
val path = requestLine.split(" ").getOrNull(1) ?: continue
|
||||
|
||||
if (path.startsWith("/callback")) {
|
||||
val html = """
|
||||
<html><body>
|
||||
<script>
|
||||
var hash = window.location.hash.substring(1);
|
||||
window.location.href = "/token?" + hash;
|
||||
</script>
|
||||
</body></html>
|
||||
""".trimIndent()
|
||||
writer.println("HTTP/1.1 200 OK")
|
||||
writer.println("Content-Type: text/html")
|
||||
writer.println("Connection: close")
|
||||
writer.println()
|
||||
writer.println(html)
|
||||
writer.flush()
|
||||
} else if (path.startsWith("/token")) {
|
||||
val query = path.substringAfter("?")
|
||||
val params = query.split("&").associate { pair ->
|
||||
val idx = pair.indexOf("=")
|
||||
if (idx >= 0) {
|
||||
pair.substring(0, idx) to URLDecoder.decode(pair.substring(idx + 1), "UTF-8")
|
||||
} else pair to ""
|
||||
}
|
||||
val token = params["access_token"]
|
||||
deferred.complete(token)
|
||||
|
||||
writer.println("HTTP/1.1 200 OK")
|
||||
writer.println("Content-Type: text/html")
|
||||
writer.println("Connection: close")
|
||||
writer.println()
|
||||
writer.println("<html><body><h1>Authentication complete. You can close this window.</h1></body></html>")
|
||||
writer.flush()
|
||||
socket.close()
|
||||
break
|
||||
}
|
||||
socket.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
deferred.complete(null)
|
||||
} finally {
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
|
||||
Desktop.getDesktop().browse(URI(fullLoginUrl))
|
||||
|
||||
return deferred.await()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user