feat: reboot button in Settings with password confirmation
- system.reboot RPC endpoint requires password re-verification - Uses systemd path unit pattern (tor-helper.sh) for privilege escalation - 2-second delay before reboot to allow RPC response to reach client - Clean UI: password input modal, loading state, error feedback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -705,6 +705,7 @@ impl RpcHandler {
|
||||
"system.detect-usb-devices" => self.handle_system_detect_usb_devices().await,
|
||||
"system.disk-status" => self.handle_system_disk_status().await,
|
||||
"system.disk-cleanup" => self.handle_system_disk_cleanup().await,
|
||||
"system.reboot" => self.handle_system_reboot(params).await,
|
||||
"system.factory-reset" => self.handle_system_factory_reset(params).await,
|
||||
|
||||
// Opt-in anonymous analytics
|
||||
|
||||
@@ -642,6 +642,38 @@ async fn read_temperatures() -> Result<Vec<serde_json::Value>> {
|
||||
impl RpcHandler {
|
||||
/// system.factory-reset — Wipe all user data, remove containers, and restart.
|
||||
/// Only preserves the data_dir itself (recreated empty on restart).
|
||||
/// system.reboot — Reboot the machine. Requires password re-verification.
|
||||
pub(super) async fn handle_system_reboot(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let password = params
|
||||
.as_ref()
|
||||
.and_then(|p| p.get("password"))
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing password — re-authentication required"))?;
|
||||
|
||||
let valid = self.auth_manager.verify_password(password).await?;
|
||||
if !valid {
|
||||
return Err(anyhow::anyhow!("Password incorrect"));
|
||||
}
|
||||
|
||||
info!("System reboot initiated by user");
|
||||
|
||||
// Schedule reboot in 2 seconds (gives time for the RPC response to reach the client)
|
||||
// Uses the tor-helper path unit pattern (writes action file, systemd triggers root service)
|
||||
tokio::spawn(async {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
let action = serde_json::json!({"action": "reboot"});
|
||||
let _ = tokio::fs::write(
|
||||
"/var/lib/archipelago/tor-config/tor-action",
|
||||
serde_json::to_string(&action).unwrap_or_default(),
|
||||
).await;
|
||||
});
|
||||
|
||||
Ok(serde_json::json!({ "rebooting": true }))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_system_factory_reset(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
|
||||
@@ -991,6 +991,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reboot Section -->
|
||||
<div class="path-option-card px-6 py-6 mt-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white/90 mb-1">Reboot</h2>
|
||||
<p class="text-sm text-white/60">Restart the machine. All containers will restart automatically.</p>
|
||||
</div>
|
||||
<button
|
||||
class="glass-button px-6 py-2 text-sm"
|
||||
:disabled="rebooting"
|
||||
@click="showRebootConfirm = true"
|
||||
>
|
||||
{{ rebooting ? 'Rebooting...' : 'Reboot' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reboot Confirmation Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showRebootConfirm" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showRebootConfirm = false">
|
||||
<div class="glass-card px-8 py-8 max-w-md mx-4">
|
||||
<h3 class="text-lg font-semibold text-white/90 mb-3">Reboot Node</h3>
|
||||
<p class="text-sm text-white/60 mb-4">Enter your password to confirm reboot. The node will be temporarily unavailable.</p>
|
||||
<input
|
||||
v-model="rebootPassword"
|
||||
type="password"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500 mb-4"
|
||||
placeholder="Password"
|
||||
@keydown.enter="performReboot"
|
||||
/>
|
||||
<p v-if="rebootError" class="text-sm text-red-400 mb-3">{{ rebootError }}</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button class="glass-button" @click="showRebootConfirm = false">Cancel</button>
|
||||
<button
|
||||
class="glass-button px-6"
|
||||
:disabled="rebooting || !rebootPassword"
|
||||
@click="performReboot"
|
||||
>
|
||||
{{ rebooting ? 'Rebooting...' : 'Confirm Reboot' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Factory Reset Section -->
|
||||
<div class="path-option-card px-6 py-6 mt-6 border-red-500/30">
|
||||
<h2 class="text-xl font-semibold text-red-400/90 mb-3">Factory Reset</h2>
|
||||
@@ -1048,6 +1093,25 @@ const router = useRouter()
|
||||
const { t, locale } = useI18n()
|
||||
const store = useAppStore()
|
||||
|
||||
// Reboot
|
||||
const showRebootConfirm = ref(false)
|
||||
const rebooting = ref(false)
|
||||
const rebootPassword = ref('')
|
||||
const rebootError = ref('')
|
||||
async function performReboot() {
|
||||
if (!rebootPassword.value) return
|
||||
rebooting.value = true
|
||||
rebootError.value = ''
|
||||
try {
|
||||
await rpcClient.call({ method: 'system.reboot', params: { password: rebootPassword.value } })
|
||||
showRebootConfirm.value = false
|
||||
rebootPassword.value = ''
|
||||
} catch (e) {
|
||||
rebootError.value = e instanceof Error ? e.message : 'Reboot failed'
|
||||
rebooting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Factory Reset
|
||||
const showFactoryResetConfirm = ref(false)
|
||||
const factoryResetLoading = ref(false)
|
||||
|
||||
@@ -109,6 +109,13 @@ case "$ACTION_TYPE" in
|
||||
write_result '{"ok":true}'
|
||||
;;
|
||||
|
||||
reboot)
|
||||
write_result '{"ok":true}'
|
||||
log "System reboot initiated"
|
||||
sleep 1
|
||||
systemctl reboot
|
||||
;;
|
||||
|
||||
*)
|
||||
log "Unknown action: $ACTION_TYPE"
|
||||
write_result '{"ok":false,"error":"Unknown action"}'
|
||||
|
||||
Reference in New Issue
Block a user