Repair Tailscale authorization flow
This commit is contained in:
@@ -14,10 +14,11 @@ app=${1:-}
|
|||||||
|
|
||||||
address=$(
|
address=$(
|
||||||
hyprctl clients -j 2>/dev/null |
|
hyprctl clients -j 2>/dev/null |
|
||||||
jq -r --arg name "${app,,}" \
|
jq -r --arg pattern "$app" \
|
||||||
'[.[] | select((.class // "") | ascii_downcase | startswith($name))] | first.address // empty'
|
'[.[] | select((.class // "") | test($pattern; "i"))] | first.address // empty'
|
||||||
)
|
)
|
||||||
|
|
||||||
[[ -n $address ]] || exit 1
|
[[ -n $address ]] || exit 1
|
||||||
|
|
||||||
hyprctl dispatch focuswindow "address:$address" >/dev/null
|
hyprctl dispatch "hl.dsp.focus({ window = \"address:$address\" })" >/dev/null 2>&1 || \
|
||||||
|
hyprctl dispatch focuswindow "address:$address" >/dev/null
|
||||||
|
|||||||
@@ -17,3 +17,7 @@ fi
|
|||||||
systemd-run --user --quiet --collect --unit="omarchy-browser-$(date +%s%N)" \
|
systemd-run --user --quiet --collect --unit="omarchy-browser-$(date +%s%N)" \
|
||||||
--property=StandardOutput=null --property=StandardError=null \
|
--property=StandardOutput=null --property=StandardError=null \
|
||||||
uwsm-app -- "$browser_exec" "${@/--private/$private_flag}"
|
uwsm-app -- "$browser_exec" "${@/--private/$private_flag}"
|
||||||
|
|
||||||
|
if [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then
|
||||||
|
omarchy-hyprland-focus-app "^$(basename "$browser_exec" -stable).*$" || true
|
||||||
|
fi
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ function accountLabel(account) {
|
|||||||
return String(account.id || "Unknown account")
|
return String(account.id || "Unknown account")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loginPlan(needsLogin, authUrl) {
|
||||||
|
var url = String(authUrl || "").trim()
|
||||||
|
if (needsLogin === true && /^https?:\/\//.test(url)) {
|
||||||
|
return { authUrl: url, command: [] }
|
||||||
|
}
|
||||||
|
return { authUrl: "", command: ["tailscale", "up"] }
|
||||||
|
}
|
||||||
|
|
||||||
function peerFromStatus(id, peer) {
|
function peerFromStatus(id, peer) {
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -270,6 +278,7 @@ if (typeof module !== "undefined") {
|
|||||||
displayHostName: displayHostName,
|
displayHostName: displayHostName,
|
||||||
osIcon: osIcon,
|
osIcon: osIcon,
|
||||||
accountLabel: accountLabel,
|
accountLabel: accountLabel,
|
||||||
|
loginPlan: loginPlan,
|
||||||
isMullvadPeer: isMullvadPeer,
|
isMullvadPeer: isMullvadPeer,
|
||||||
peerFromStatus: peerFromStatus,
|
peerFromStatus: peerFromStatus,
|
||||||
parseExitNodeList: parseExitNodeList,
|
parseExitNodeList: parseExitNodeList,
|
||||||
|
|||||||
@@ -498,6 +498,56 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CursorSurface {
|
||||||
|
id: connectionRow
|
||||||
|
visible: tailscale.installed && !tailscale.running
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: connectionText.implicitHeight + Style.spacing.rowPaddingX
|
||||||
|
hasCursor: root.cursorActive && root.focusSection === "header"
|
||||||
|
foreground: root.foreground
|
||||||
|
fill: root.hoverFill
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: !tailscale.busy
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onEntered: {
|
||||||
|
root.cursorActive = true
|
||||||
|
root.focusSection = "header"
|
||||||
|
}
|
||||||
|
onClicked: tailscale.loginOrUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: connectionText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Style.space(12)
|
||||||
|
anchors.rightMargin: Style.space(12)
|
||||||
|
spacing: Style.space(2)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: tailscale.needsLogin ? "Authorize this device" : "Connect Tailscale"
|
||||||
|
color: root.foreground
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: tailscale.needsLogin ? "Open Tailscale to restore this device's access" : "Reconnect with the current Tailscale settings"
|
||||||
|
color: root.dim
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -244,15 +244,19 @@ Item {
|
|||||||
|
|
||||||
function loginOrUp() {
|
function loginOrUp() {
|
||||||
if (!installed || loginProcess.running) return
|
if (!installed || loginProcess.running) return
|
||||||
|
var plan = Model.loginPlan(needsLogin, authUrl)
|
||||||
|
if (plan.authUrl !== "") {
|
||||||
|
_loginUrlOpened = false
|
||||||
|
openAuthUrlFrom(plan.authUrl, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
_loginOutput = ""
|
_loginOutput = ""
|
||||||
_loginError = ""
|
_loginError = ""
|
||||||
actionStatus = needsLogin ? "Starting Tailscale login…" : "Turning Tailscale on…"
|
actionStatus = needsLogin ? "Starting Tailscale login…" : "Turning Tailscale on…"
|
||||||
_loginInProgress = needsLogin
|
_loginInProgress = needsLogin
|
||||||
_loginUrlOpened = false
|
_loginUrlOpened = false
|
||||||
_preLoginAuthUrl = authUrl
|
_preLoginAuthUrl = authUrl
|
||||||
var command = ["tailscale", "up"]
|
loginProcess.command = plan.command
|
||||||
if (needsLogin) command.push("--force-reauth")
|
|
||||||
loginProcess.command = command
|
|
||||||
loginProcess.running = true
|
loginProcess.running = true
|
||||||
if (needsLogin) loginTimeoutTimer.restart()
|
if (needsLogin) loginTimeoutTimer.restart()
|
||||||
}
|
}
|
||||||
@@ -317,9 +321,7 @@ Item {
|
|||||||
_loginUrlOpened = true
|
_loginUrlOpened = true
|
||||||
_loginInProgress = false
|
_loginInProgress = false
|
||||||
loginTimeoutTimer.stop()
|
loginTimeoutTimer.stop()
|
||||||
Qt.openUrlExternally(url)
|
Quickshell.execDetached(["omarchy-launch-browser", url])
|
||||||
actionStatus = "Opened login link"
|
|
||||||
actionStatusTimer.restart()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
test_tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$test_tmp"' EXIT
|
||||||
|
|
||||||
|
mock_bin="$test_tmp/bin"
|
||||||
|
mkdir -p "$mock_bin"
|
||||||
|
|
||||||
|
cat >"$mock_bin/hyprctl" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
if [[ $1 == "clients" ]]; then
|
||||||
|
printf '[{"address":"0xabc","class":"chromium"}]\n'
|
||||||
|
elif [[ $1 == "dispatch" ]]; then
|
||||||
|
printf '%s\n' "$2" >"$OMARCHY_TEST_FOCUS_DISPATCH"
|
||||||
|
fi
|
||||||
|
SH
|
||||||
|
chmod +x "$mock_bin/hyprctl"
|
||||||
|
|
||||||
|
dispatch_log="$test_tmp/dispatch"
|
||||||
|
PATH="$mock_bin:$PATH" OMARCHY_TEST_FOCUS_DISPATCH="$dispatch_log" \
|
||||||
|
bash "$ROOT/bin/omarchy-hyprland-focus-app" chromium
|
||||||
|
|
||||||
|
grep -F 'hl.dsp.focus({ window = "address:0xabc" })' "$dispatch_log" >/dev/null || \
|
||||||
|
fail "app focus uses the workspace-aware Hyprland dispatcher"
|
||||||
|
|
||||||
|
pass "app focus follows windows across workspaces"
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
test_tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$test_tmp"' EXIT
|
||||||
|
|
||||||
|
mock_bin="$test_tmp/bin"
|
||||||
|
test_home="$test_tmp/home"
|
||||||
|
mkdir -p "$mock_bin" "$test_home/.local/share/applications"
|
||||||
|
|
||||||
|
cat >"$test_home/.local/share/applications/chromium.desktop" <<'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Exec=chromium %U
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >"$mock_bin/xdg-settings" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
echo chromium.desktop
|
||||||
|
SH
|
||||||
|
cat >"$mock_bin/chromium" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
cat >"$mock_bin/systemd-run" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
printf '%s\n' "$*" >"$OMARCHY_TEST_BROWSER_LAUNCH"
|
||||||
|
SH
|
||||||
|
cat >"$mock_bin/omarchy-hyprland-focus-app" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
printf '%s\n' "$1" >"$OMARCHY_TEST_BROWSER_FOCUS"
|
||||||
|
SH
|
||||||
|
chmod +x "$mock_bin"/*
|
||||||
|
|
||||||
|
launch_log="$test_tmp/launch"
|
||||||
|
focus_log="$test_tmp/focus"
|
||||||
|
HOME="$test_home" PATH="$mock_bin:$PATH" HYPRLAND_INSTANCE_SIGNATURE=test \
|
||||||
|
OMARCHY_TEST_BROWSER_LAUNCH="$launch_log" OMARCHY_TEST_BROWSER_FOCUS="$focus_log" \
|
||||||
|
bash "$ROOT/bin/omarchy-launch-browser" "https://example.test/authorize"
|
||||||
|
|
||||||
|
grep -F 'https://example.test/authorize' "$launch_log" >/dev/null || fail "browser launcher passes through the URL"
|
||||||
|
grep -Fx '^chromium.*$' "$focus_log" >/dev/null || fail "browser launcher focuses the default browser window"
|
||||||
|
|
||||||
|
pass "browser launcher follows opened links to the browser workspace"
|
||||||
@@ -165,6 +165,22 @@ assertEqual(
|
|||||||
'tailscale labels connections by tailnet when nickname is missing'
|
'tailscale labels connections by tailnet when nickname is missing'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertDeepEqual(
|
||||||
|
tailscale.loginPlan(true, 'https://login.tailscale.com/a/existing'),
|
||||||
|
{ authUrl: 'https://login.tailscale.com/a/existing', command: [] },
|
||||||
|
'tailscale reuses the daemon authorization URL without replacing node identity'
|
||||||
|
)
|
||||||
|
assertDeepEqual(
|
||||||
|
tailscale.loginPlan(true, ''),
|
||||||
|
{ authUrl: '', command: ['tailscale', 'up'] },
|
||||||
|
'tailscale requests a login URL when the daemon has not supplied one'
|
||||||
|
)
|
||||||
|
assertDeepEqual(
|
||||||
|
tailscale.loginPlan(false, 'https://login.tailscale.com/a/stale'),
|
||||||
|
{ authUrl: '', command: ['tailscale', 'up'] },
|
||||||
|
'tailscale ignores stale authorization URLs outside the login state'
|
||||||
|
)
|
||||||
|
|
||||||
assertDeepEqual(tailscale.parseStatus('{'), { ok: false, unavailable: true, message: 'Status error', error: 'Failed to parse tailscale status' }, 'tailscale reports invalid status JSON')
|
assertDeepEqual(tailscale.parseStatus('{'), { ok: false, unavailable: true, message: 'Status error', error: 'Failed to parse tailscale status' }, 'tailscale reports invalid status JSON')
|
||||||
assertDeepEqual(tailscale.parseAccounts('{'), { accounts: [], selectedAccountId: '', selectedAccountLabel: '' }, 'tailscale handles invalid account JSON')
|
assertDeepEqual(tailscale.parseAccounts('{'), { accounts: [], selectedAccountId: '', selectedAccountLabel: '' }, 'tailscale handles invalid account JSON')
|
||||||
JS
|
JS
|
||||||
|
|||||||
Reference in New Issue
Block a user