Files
AuraVPN/aura-gui/src/App.css
T
xah30 1635190797 feat(aura-gui): privilege escalation via sudo + one-click NOPASSWD installer
The v0.1 GUI's Connect button was broken in practice: the Tauri app launched
from /Applications runs as the desktop user, so `Command::new(aura).spawn()`
started aura without root. aura died in ms with EPERM at TUN creation, faster
than the 1.5 s status poller could catch — the UI just silently flipped back
to "disconnected" with no clue.

## Fix

* `cli_proc::spawn_client` now prepends `sudo -n` on Unix. After spawn it
  blocks for 1.5 s and checks `try_wait`; if the child already exited, it
  reads the stderr ring's last 20 lines and returns an anyhow Error with
  that tail + a hint list of common causes. The Tauri command surfaces it
  to the frontend's `error` state where the UI renders it as a multi-line
  `<pre>` block instead of the previous single-line text.
* `ClientHandle::kill` no longer uses `Child::kill` (SIGKILL) on its sudo
  parent — that would have left aura orphaned with the TUN lingering.
  Sends SIGTERM to sudo, which sudo forwards to aura, giving the inner
  `OsRouteGuard::Drop` 2 s to run cleanup. Falls back to SIGKILL only after
  the grace period.

## One-click NOPASSWD installer

Two new Tauri commands plus a UI banner:

* `check_admin_access` — runs `sudo -n aura --help` and returns whether the
  sudoers entry is in place. Used by the React side to decide whether to
  show the banner.
* `install_sudoers_admin` — runs `osascript ... with administrator
  privileges` which surfaces the native macOS auth dialog, then writes
  `/etc/sudoers.d/aura-gui` scoped to `<aura> client *` only (not arbitrary
  aura invocations), runs `visudo -c` for syntax validation, and reports
  success or the syntax error.

The frontend shows a yellow "One-time setup needed" banner above the
profile list whenever `adminReady === false`. Clicking the button pops the
Mac password dialog once; from then on Connect is a single click with no
prompt.

## UI feedback

* "Connecting…" disabled state on the Connect button while spawn_client's
  1.5 s wait is in progress
* Errors render as monospace `<pre>` so the multi-line stderr tail is
  readable
* `.error` and `.admin-banner` CSS classes added to App.css

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:32:38 +03:00

313 lines
5.1 KiB
CSS

/* AuraVPN GUI — dark-mode by default, dense single-pane VPN dashboard. */
:root {
--bg: #0f1115;
--panel-bg: #1a1d24;
--border: #2a2f3a;
--text: #d9dde4;
--text-dim: #8a92a3;
--accent: #5ad3aa;
--accent-hot: #4ac09a;
--danger: #ef5a5a;
--warn: #f7b955;
--bad: #ef5a5a;
font-family: -apple-system, "Segoe UI", Inter, Avenir, Helvetica, Arial,
sans-serif;
font-size: 14px;
line-height: 1.45;
color: var(--text);
background-color: var(--bg);
}
body {
margin: 0;
}
.container {
max-width: 760px;
margin: 0 auto;
padding: 24px 24px 48px;
display: flex;
flex-direction: column;
gap: 16px;
}
header {
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
header h1 {
margin: 0;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.02em;
}
header .sub {
margin: 4px 0 0;
color: var(--text-dim);
font-size: 13px;
}
.pill {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.pill.running {
background: rgba(90, 211, 170, 0.18);
color: var(--accent);
}
.pill.stopped {
background: rgba(138, 146, 163, 0.18);
color: var(--text-dim);
}
.panel {
background: var(--panel-bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
}
.panel.small {
padding: 10px 14px;
}
.panel h2 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--text-dim);
text-transform: uppercase;
}
.row-between {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.row-between h2 {
margin: 0;
}
.empty {
margin: 12px 0;
color: var(--text-dim);
font-style: italic;
}
button {
background: #2a2f3a;
color: var(--text);
border: 1px solid #2a2f3a;
border-radius: 6px;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
button:hover:not(:disabled) {
background: #353a45;
border-color: #404552;
}
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
button.primary {
background: var(--accent);
color: #0f1115;
border-color: var(--accent);
}
button.primary:hover:not(:disabled) {
background: var(--accent-hot);
border-color: var(--accent-hot);
}
button.danger {
background: var(--danger);
color: white;
border-color: var(--danger);
}
button.danger:hover:not(:disabled) {
background: #d44e4e;
border-color: #d44e4e;
}
.profile-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.profile-list li {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 12px 14px;
background: #14171d;
border: 1px solid #232730;
border-radius: 8px;
}
.profile-list li.active {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(90, 211, 170, 0.4);
}
.profile-meta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.profile-name {
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.profile-server {
font-size: 12px;
color: var(--text-dim);
font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
}
.profile-id {
font-size: 11px;
color: var(--text-dim);
}
.profile-actions {
display: flex;
gap: 8px;
}
.badge {
font-size: 10px;
font-weight: 500;
padding: 1px 6px;
border-radius: 4px;
text-transform: uppercase;
}
.badge.bad {
background: rgba(239, 90, 90, 0.15);
color: var(--bad);
}
.status {
border-collapse: collapse;
width: 100%;
}
.status td {
padding: 6px 8px;
border-bottom: 1px solid #232730;
font-size: 13px;
}
.status td:first-child {
width: 40%;
color: var(--text-dim);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.04em;
}
.status td.warn {
color: var(--warn);
}
.logs {
background: #0a0c10;
border: 1px solid #232730;
border-radius: 6px;
padding: 10px 12px;
font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
font-size: 11px;
max-height: 260px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
color: #c6cbd5;
}
.error {
background: rgba(239, 90, 90, 0.12);
border: 1px solid rgba(239, 90, 90, 0.4);
border-radius: 8px;
padding: 12px 14px;
color: #ffb1b1;
display: flex;
flex-direction: column;
gap: 10px;
}
.error-body {
margin: 0;
font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
font-size: 11px;
color: #ffc8c8;
white-space: pre-wrap;
word-break: break-word;
max-height: 200px;
overflow: auto;
}
.error button {
align-self: flex-end;
}
.admin-banner {
background: rgba(247, 185, 85, 0.12);
border: 1px solid rgba(247, 185, 85, 0.4);
border-radius: 8px;
padding: 14px 16px;
color: #f7b955;
display: flex;
align-items: center;
gap: 16px;
font-size: 13px;
}
.admin-banner > div {
flex: 1;
}
.admin-banner button {
flex-shrink: 0;
}
.aura-bin code {
font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
font-size: 12px;
color: var(--text-dim);
}