mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-08 14:33:54 +00:00
fix(mobile): authenticate terminal page with stored operator token
This commit is contained in:
@@ -661,6 +661,7 @@ class NodeRuntime(
|
||||
_gatewayUpdateAvailable.value = hello.updateAvailable
|
||||
_seamColorArgb.value = DEFAULT_SEAM_COLOR_ARGB
|
||||
syncMainSessionKey(resolveAgentIdFromMainSessionKey(hello.mainSessionKey))
|
||||
refreshGatewayControlPage()
|
||||
updateStatus {
|
||||
operatorConnectionProblem = null
|
||||
operatorConnected = true
|
||||
@@ -1872,6 +1873,7 @@ class NodeRuntime(
|
||||
activeGatewayAuth = auth
|
||||
val tls = connectionManager.resolveTlsParams(endpoint)
|
||||
val storedOperatorEntry = loadStoredRoleDeviceAuthEntry("operator")
|
||||
refreshGatewayControlPage(endpoint, auth, storedOperatorEntry?.token)
|
||||
val usesStoredOperatorDeviceToken =
|
||||
operatorSessionUsesStoredDeviceToken(auth, storedOperatorEntry?.token)
|
||||
val operatorAuth =
|
||||
@@ -1971,6 +1973,24 @@ class NodeRuntime(
|
||||
|
||||
private fun isCurrentConnectAttempt(connectAttemptId: Long): Boolean = connectAttemptSeq.get() == connectAttemptId
|
||||
|
||||
private fun refreshGatewayControlPage(
|
||||
endpoint: GatewayEndpoint? = connectedEndpoint,
|
||||
auth: GatewayConnectAuth = activeGatewayAuth ?: resolveGatewayConnectAuth(),
|
||||
storedOperatorToken: String? = loadStoredRoleDeviceAuthEntry("operator")?.token,
|
||||
) {
|
||||
if (endpoint == null) {
|
||||
_gatewayControlPage.value = null
|
||||
return
|
||||
}
|
||||
val pageAuth = resolveGatewayControlPageAuth(auth, storedOperatorToken)
|
||||
_gatewayControlPage.value =
|
||||
GatewayControlPage(
|
||||
baseUrl = gatewayControlPageBaseUrl(endpoint),
|
||||
token = pageAuth.token,
|
||||
password = pageAuth.password,
|
||||
)
|
||||
}
|
||||
|
||||
private fun connectAfterTlsCheck(
|
||||
endpoint: GatewayEndpoint,
|
||||
auth: GatewayConnectAuth,
|
||||
@@ -1978,12 +1998,6 @@ class NodeRuntime(
|
||||
) {
|
||||
if (!isCurrentConnectAttempt(connectAttemptId)) return
|
||||
connectedEndpoint = endpoint
|
||||
_gatewayControlPage.value =
|
||||
GatewayControlPage(
|
||||
baseUrl = gatewayControlPageBaseUrl(endpoint),
|
||||
token = auth.token?.trim()?.takeIf { it.isNotEmpty() },
|
||||
password = auth.password?.trim()?.takeIf { it.isNotEmpty() },
|
||||
)
|
||||
updateStatus {
|
||||
operatorConnectionProblem = null
|
||||
nodeConnectionProblem = null
|
||||
@@ -3478,6 +3492,44 @@ internal fun resolveOperatorSessionConnectAuth(
|
||||
)
|
||||
}
|
||||
|
||||
internal fun resolveGatewayControlPageAuth(
|
||||
auth: NodeRuntime.GatewayConnectAuth,
|
||||
storedOperatorToken: String?,
|
||||
): NodeRuntime.GatewayConnectAuth {
|
||||
val explicitToken = auth.token?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (explicitToken != null) {
|
||||
return NodeRuntime.GatewayConnectAuth(
|
||||
token = explicitToken,
|
||||
bootstrapToken = null,
|
||||
password = null,
|
||||
)
|
||||
}
|
||||
|
||||
val explicitPassword = auth.password?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (explicitPassword != null) {
|
||||
return NodeRuntime.GatewayConnectAuth(
|
||||
token = null,
|
||||
bootstrapToken = null,
|
||||
password = explicitPassword,
|
||||
)
|
||||
}
|
||||
|
||||
val storedToken = storedOperatorToken?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (storedToken != null) {
|
||||
return NodeRuntime.GatewayConnectAuth(
|
||||
token = storedToken,
|
||||
bootstrapToken = null,
|
||||
password = null,
|
||||
)
|
||||
}
|
||||
|
||||
return NodeRuntime.GatewayConnectAuth(
|
||||
token = null,
|
||||
bootstrapToken = null,
|
||||
password = null,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun operatorSessionUsesStoredDeviceToken(
|
||||
auth: NodeRuntime.GatewayConnectAuth,
|
||||
storedOperatorToken: String?,
|
||||
|
||||
@@ -213,6 +213,48 @@ class GatewayBootstrapAuthTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveGatewayControlPageAuthFallsBackToStoredOperatorToken() {
|
||||
val resolved =
|
||||
resolveGatewayControlPageAuth(
|
||||
auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
|
||||
storedOperatorToken = " stored-token ",
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
NodeRuntime.GatewayConnectAuth(token = "stored-token", bootstrapToken = null, password = null),
|
||||
resolved,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveGatewayControlPageAuthPrefersExplicitSharedAuth() {
|
||||
assertEquals(
|
||||
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = null, password = null),
|
||||
resolveGatewayControlPageAuth(
|
||||
auth =
|
||||
NodeRuntime.GatewayConnectAuth(
|
||||
token = " shared-token ",
|
||||
bootstrapToken = "bootstrap-1",
|
||||
password = "shared-password",
|
||||
),
|
||||
storedOperatorToken = "stored-token",
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = null, password = "shared-password"),
|
||||
resolveGatewayControlPageAuth(
|
||||
auth =
|
||||
NodeRuntime.GatewayConnectAuth(
|
||||
token = null,
|
||||
bootstrapToken = "bootstrap-1",
|
||||
password = " shared-password ",
|
||||
),
|
||||
storedOperatorToken = "stored-token",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun operatorConnectScopesForAuthUsesNativeScopesWhenNoStoredOperatorMetadata() {
|
||||
assertEquals(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import OpenClawKit
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
@@ -22,13 +23,20 @@ struct TerminalHubScreen: View {
|
||||
|
||||
var body: some View {
|
||||
let config = self.appModel.activeGatewayConnectConfig
|
||||
let storedOperatorToken = config == nil ? nil : Self.storedOperatorToken()
|
||||
ZStack {
|
||||
OpenClawProBackground()
|
||||
if let url = Self.terminalURL(config: config) {
|
||||
TerminalWebView(url: url, authScript: Self.terminalAuthUserScript(config: config))
|
||||
TerminalWebView(
|
||||
url: url,
|
||||
authScript: Self.terminalAuthUserScript(
|
||||
config: config,
|
||||
storedOperatorToken: storedOperatorToken))
|
||||
// Recreate the web view only when the connection inputs
|
||||
// change; SwiftUI update passes must not restart live shells.
|
||||
.id(Self.webContentIdentity(config: config))
|
||||
.id(Self.webContentIdentity(
|
||||
config: config,
|
||||
storedOperatorToken: storedOperatorToken))
|
||||
.ignoresSafeArea(.container, edges: .bottom)
|
||||
} else {
|
||||
self.unavailableCard
|
||||
@@ -97,14 +105,24 @@ struct TerminalHubScreen: View {
|
||||
/// (the same mechanism the macOS Dashboard window uses), so the token never
|
||||
/// appears in the page URL, WebKit history, or gateway request logs.
|
||||
static func terminalAuthUserScript(config: GatewayConnectConfig?) -> String? {
|
||||
self.terminalAuthUserScript(config: config, storedOperatorToken: self.storedOperatorToken())
|
||||
}
|
||||
|
||||
static func terminalAuthUserScript(
|
||||
config: GatewayConnectConfig?,
|
||||
storedOperatorToken: String?) -> String?
|
||||
{
|
||||
guard let config, let pageURL = terminalURL(config: config) else {
|
||||
return nil
|
||||
}
|
||||
var payload: [String: String] = ["gatewayUrl": config.url.absoluteString]
|
||||
let token = config.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let storedToken = storedOperatorToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let password = config.password?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !token.isEmpty {
|
||||
payload["token"] = token
|
||||
} else if !storedToken.isEmpty {
|
||||
payload["token"] = storedToken
|
||||
}
|
||||
if !password.isEmpty {
|
||||
payload["password"] = password
|
||||
@@ -134,13 +152,24 @@ struct TerminalHubScreen: View {
|
||||
/// Identity for the embedded web view: recreate it only when the gateway
|
||||
/// endpoint or credentials actually change.
|
||||
static func webContentIdentity(config: GatewayConnectConfig?) -> Int {
|
||||
self.webContentIdentity(config: config, storedOperatorToken: self.storedOperatorToken())
|
||||
}
|
||||
|
||||
static func webContentIdentity(config: GatewayConnectConfig?, storedOperatorToken: String?) -> Int {
|
||||
var hasher = Hasher()
|
||||
hasher.combine(config?.url)
|
||||
hasher.combine(config?.token)
|
||||
hasher.combine(config?.password)
|
||||
hasher.combine(storedOperatorToken?.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
return hasher.finalize()
|
||||
}
|
||||
|
||||
private static func storedOperatorToken() -> String? {
|
||||
let identity = DeviceIdentityStore.loadOrCreate()
|
||||
return DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator")?
|
||||
.token
|
||||
}
|
||||
|
||||
static func originString(for url: URL) -> String {
|
||||
guard let scheme = url.scheme, let host = url.host else {
|
||||
return ""
|
||||
|
||||
@@ -64,10 +64,33 @@ struct TerminalHubScreenTests {
|
||||
#expect(script?.contains("\"gatewayUrl\":\"wss:\\/\\/gateway.example.com:8443\"") == true)
|
||||
}
|
||||
|
||||
@Test func `auth user script falls back to stored operator token`() throws {
|
||||
let config = try Self.makeConfig(
|
||||
url: #require(URL(string: "wss://gateway.example.com:8443")),
|
||||
token: nil,
|
||||
password: nil)
|
||||
|
||||
let script = TerminalHubScreen.terminalAuthUserScript(
|
||||
config: config,
|
||||
storedOperatorToken: " stored-token ")
|
||||
|
||||
#expect(script?.contains("\"token\":\"stored-token\"") == true)
|
||||
}
|
||||
|
||||
@Test func `web content identity changes with stored operator token`() throws {
|
||||
let config = try Self.makeConfig(url: #require(URL(string: "wss://gateway.example.com")))
|
||||
|
||||
#expect(
|
||||
TerminalHubScreen.webContentIdentity(config: config, storedOperatorToken: "token-a") !=
|
||||
TerminalHubScreen.webContentIdentity(config: config, storedOperatorToken: "token-b"))
|
||||
}
|
||||
|
||||
@Test func `auth user script is omitted without credentials`() throws {
|
||||
let config = try Self.makeConfig(url: #require(URL(string: "wss://gateway.example.com")), token: " ")
|
||||
|
||||
#expect(TerminalHubScreen.terminalAuthUserScript(config: config) == nil)
|
||||
#expect(TerminalHubScreen.terminalAuthUserScript(config: nil) == nil)
|
||||
#expect(
|
||||
TerminalHubScreen.terminalAuthUserScript(config: config, storedOperatorToken: nil) == nil)
|
||||
#expect(
|
||||
TerminalHubScreen.terminalAuthUserScript(config: nil, storedOperatorToken: nil) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user