v0.7.0: Login script dropdown, batch address book, Docker non-root

New features:
- Login script selector: dropdown populated from server scripts dir (#52)
- Batch address book endpoint eliminates N+1 API calls (#56)
- Clone button for address book entries (#56)
- Increased API rate limits (#56)

Fixes:
- Docker container runs as non-root user (#50)
- Conditional --no-sandbox when running as root (#50)
- Post-spawn Chromium liveness check with stderr capture (#50)

Docs:
- Theme/branding configuration guide (#55)
- Vault metadata policy for deletes (#54)
- TLS config clarification (no boolean toggle)
This commit is contained in:
Dave Kempe
2026-03-11 21:54:38 +11:00
parent a57af11413
commit d07b8ae225
5 changed files with 67 additions and 5 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustguac"
version = "0.6.2"
version = "0.7.0"
edition = "2021"
description = "Lightweight Rust replacement for Apache Guacamole client"
+21
View File
@@ -195,6 +195,27 @@ pub async fn delete_session(
}
}
/// GET /api/login-scripts — List available login scripts. Requires operator+.
pub async fn list_login_scripts(State(manager): State<AppState>) -> impl IntoResponse {
let scripts_dir = std::path::Path::new(&manager.config().login_scripts_dir);
let mut scripts: Vec<String> = Vec::new();
if let Ok(entries) = std::fs::read_dir(scripts_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
// Only list executable-looking scripts, skip hidden files
if !name.starts_with('.') {
scripts.push(name.to_string());
}
}
}
}
}
scripts.sort();
Json(json!({ "scripts": scripts }))
}
/// GET /api/health — Health check.
pub async fn health() -> impl IntoResponse {
Json(json!({ "status": "ok" }))
+2
View File
@@ -733,6 +733,8 @@ async fn run_server(config: Config, database: Db) {
delete(api::admin_revoke_user_token),
)
.route("/api/admin/token-audit", get(api::admin_token_audit))
// Login scripts listing
.route("/api/login-scripts", get(api::list_login_scripts))
// Address book routes
.route("/api/addressbook", get(api::ab_list_all))
.route("/api/addressbook/folders", get(api::ab_list_folders))
+5
View File
@@ -284,6 +284,11 @@ impl SessionManager {
}
}
/// Read-only access to the config.
pub fn config(&self) -> &Config {
&self.config
}
/// Create a new session: connect to guacd, perform handshake, return session info.
pub async fn create_session(
&self,
+38 -4
View File
@@ -453,9 +453,11 @@
<input type="password" id="em-web-password">
</label>
<label>Login Script <span style="color:var(--text-muted);font-size:0.85em">(optional)</span>
<input type="text" id="em-login-script" placeholder="e.g. portal-login.sh">
<select id="em-login-script">
<option value="">None</option>
</select>
</label>
<div class="field-hint">Server-side script in the scripts directory. Receives CDP port and credentials as env vars. Runs after Chromium spawns.</div>
<div class="field-hint">Server-side script from the scripts directory. Receives CDP port and credentials as env vars. Runs after Chromium spawns.</div>
<div style="margin-top:1em;padding-top:0.8em;border-top:1px solid var(--border)">
<label style="cursor:pointer;color:var(--accent);font-size:0.95em" id="em-autofill-toggle">
<span id="em-autofill-arrow">&#9654;</span> Autofill
@@ -623,6 +625,35 @@
var folderEntryCounts = {}; // "scope/name" -> count
var folderEntries = {}; // "scope/name" -> entries array (from batch load)
var currentEntries = []; // entries for the currently selected folder
var loginScriptsCache = null; // cached list from /api/login-scripts
function populateLoginScriptDropdown(selectedValue) {
var sel = document.getElementById('em-login-script');
sel.innerHTML = '<option value="">None</option>';
if (loginScriptsCache) {
loginScriptsCache.forEach(function(s) {
var opt = document.createElement('option');
opt.value = s;
opt.textContent = s;
sel.appendChild(opt);
});
}
sel.value = selectedValue || '';
}
function loadLoginScripts(cb) {
if (loginScriptsCache !== null) { if (cb) cb(); return; }
fetch('/api/login-scripts', { headers: authHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
loginScriptsCache = data.scripts || [];
if (cb) cb();
})
.catch(function() {
loginScriptsCache = [];
if (cb) cb();
});
}
function showError(msg) {
document.getElementById('global-error').textContent = msg;
@@ -1190,7 +1221,10 @@
if (val === 'ssh') document.getElementById('em-ssh-fields').style.display = '';
else if (val === 'rdp') document.getElementById('em-rdp-fields').style.display = '';
else if (val === 'vnc') document.getElementById('em-vnc-fields').style.display = '';
else if (val === 'web') document.getElementById('em-web-fields').style.display = '';
else if (val === 'web') {
document.getElementById('em-web-fields').style.display = '';
loadLoginScripts(function() { populateLoginScriptDropdown(''); });
}
// Show drive option for SSH and RDP only
document.getElementById('em-drive-section').style.display = (val === 'ssh' || val === 'rdp') ? '' : 'none';
// Show RemoteApp for RDP only
@@ -1627,7 +1661,7 @@
} else if (type === 'web') {
document.getElementById('em-url').value = entryData.url || '';
document.getElementById('em-web-username').value = entryData.username || '';
document.getElementById('em-login-script').value = entryData.login_script || '';
loadLoginScripts(function() { populateLoginScriptDropdown(entryData.login_script || ''); });
// Populate autofill rows
if (entryData.autofill) {
try {