feat: Tauri auto-updater + RDP vault credentials + sidebar persist
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 2m55s
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 2m55s
Tauri auto-updater: - Signing pubkey in tauri.conf.json - tauri-plugin-updater initialized in lib.rs - CI workflow passes TAURI_SIGNING_PRIVATE_KEY env vars to cargo tauri build - CI generates update.json manifest with signature and uploads to packages/latest/update.json endpoint - Frontend checks for updates on startup via @tauri-apps/plugin-updater - Downloads, installs, and relaunches seamlessly - Settings → About button uses native updater too RDP vault credentials: - RDP connections now resolve credentials from vault via credentialId - Same path as SSH: list_credentials → find by ID → decrypt_password - Falls back to conn.options JSON if no vault credential linked - Fixes blank username in RDP connect Sidebar drag persist: - reorder_connections and reorder_groups Tauri commands - Batch-update sort_order in database on drop - Order survives app restart Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7c2ab2aa60
commit
0c6a4b8109
@ -61,13 +61,15 @@ jobs:
|
|||||||
$env:Path = "$env:EXTRA_PATH;$env:Path"
|
$env:Path = "$env:EXTRA_PATH;$env:Path"
|
||||||
cargo install tauri-cli --version "^2"
|
cargo install tauri-cli --version "^2"
|
||||||
|
|
||||||
- name: Build Tauri app
|
- name: Build Tauri app (with update signing)
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
$env:Path = "$env:EXTRA_PATH;$env:Path"
|
$env:Path = "$env:EXTRA_PATH;$env:Path"
|
||||||
|
$env:TAURI_SIGNING_PRIVATE_KEY = "${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}"
|
||||||
|
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}"
|
||||||
cargo tauri build
|
cargo tauri build
|
||||||
Write-Host "=== Build output ==="
|
Write-Host "=== Build output ==="
|
||||||
Get-ChildItem -Recurse src-tauri\target\release\bundle\nsis\*.exe
|
Get-ChildItem -Recurse src-tauri\target\release\bundle\nsis\*
|
||||||
|
|
||||||
- name: Download jsign
|
- name: Download jsign
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@ -122,6 +124,55 @@ jobs:
|
|||||||
|
|
||||||
Write-Host "=== Upload complete ==="
|
Write-Host "=== Upload complete ==="
|
||||||
|
|
||||||
|
- name: Generate and upload update.json for Tauri updater
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$ver = ("${{ github.ref_name }}" -replace '^v','')
|
||||||
|
$giteaUrl = "https://git.command.vigilcyber.com"
|
||||||
|
$headers = @{ Authorization = "token ${{ secrets.GIT_TOKEN }}" }
|
||||||
|
|
||||||
|
# Find the .sig file produced by Tauri signing
|
||||||
|
$sigFile = Get-ChildItem -Recurse src-tauri\target\release\bundle\nsis\*.nsis.zip.sig | Select-Object -First 1
|
||||||
|
$zipFile = Get-ChildItem -Recurse src-tauri\target\release\bundle\nsis\*.nsis.zip | Select-Object -First 1
|
||||||
|
|
||||||
|
if ($sigFile -and $zipFile) {
|
||||||
|
$signature = Get-Content $sigFile.FullName -Raw
|
||||||
|
$downloadUrl = "$giteaUrl/api/packages/vstockwell/generic/wraith/$ver/$($zipFile.Name)"
|
||||||
|
|
||||||
|
# Upload the .nsis.zip to packages
|
||||||
|
Write-Host "Uploading: $($zipFile.Name)"
|
||||||
|
Invoke-RestMethod -Uri "$giteaUrl/api/packages/vstockwell/generic/wraith/$ver/$($zipFile.Name)" -Method PUT -Headers $headers -ContentType "application/octet-stream" -InFile $zipFile.FullName
|
||||||
|
|
||||||
|
# Build update.json
|
||||||
|
$updateJson = @{
|
||||||
|
version = "v$ver"
|
||||||
|
notes = "Wraith Desktop v$ver"
|
||||||
|
pub_date = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
|
||||||
|
platforms = @{
|
||||||
|
"windows-x86_64" = @{
|
||||||
|
signature = $signature.Trim()
|
||||||
|
url = $downloadUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 4
|
||||||
|
|
||||||
|
$updateJson | Out-File update.json -Encoding utf8
|
||||||
|
Write-Host "update.json content:"
|
||||||
|
Get-Content update.json
|
||||||
|
|
||||||
|
# Upload to latest/ so the updater endpoint always points to the newest
|
||||||
|
Invoke-RestMethod -Uri "$giteaUrl/api/packages/vstockwell/generic/wraith/latest/update.json" -Method PUT -Headers $headers -ContentType "application/octet-stream" -InFile update.json
|
||||||
|
|
||||||
|
# Also upload to versioned path
|
||||||
|
Invoke-RestMethod -Uri "$giteaUrl/api/packages/vstockwell/generic/wraith/$ver/update.json" -Method PUT -Headers $headers -ContentType "application/octet-stream" -InFile update.json
|
||||||
|
|
||||||
|
Write-Host "=== Update manifest uploaded ==="
|
||||||
|
} else {
|
||||||
|
Write-Host "WARNING: No .sig file found — update signing may have failed"
|
||||||
|
Write-Host "Sig files found:"
|
||||||
|
Get-ChildItem -Recurse src-tauri\target\release\bundle\nsis\*.sig
|
||||||
|
}
|
||||||
|
|
||||||
- name: Create Release and attach installers
|
- name: Create Release and attach installers
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@ -19,7 +19,9 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.0.0",
|
"@codemirror/theme-one-dark": "^6.0.0",
|
||||||
"@codemirror/view": "^6.0.0",
|
"@codemirror/view": "^6.0.0",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-search": "^0.16.0",
|
"@xterm/addon-search": "^0.16.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
@ -1516,6 +1518,15 @@
|
|||||||
"url": "https://opencollective.com/tauri"
|
"url": "https://opencollective.com/tauri"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-process": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-shell": {
|
"node_modules/@tauri-apps/plugin-shell": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
||||||
@ -1525,6 +1536,15 @@
|
|||||||
"@tauri-apps/api": "^2.10.1"
|
"@tauri-apps/api": "^2.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-updater": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|||||||
36
package.json
36
package.json
@ -10,31 +10,33 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.0",
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
"pinia": "^3.0.0",
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/lang-javascript": "^6.0.0",
|
||||||
|
"@codemirror/lang-json": "^6.0.0",
|
||||||
|
"@codemirror/lang-markdown": "^6.0.0",
|
||||||
|
"@codemirror/lang-python": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/theme-one-dark": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-search": "^0.16.0",
|
"@xterm/addon-search": "^0.16.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@codemirror/view": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"@codemirror/state": "^6.0.0",
|
"pinia": "^3.0.0",
|
||||||
"@codemirror/commands": "^6.0.0",
|
"vue": "^3.5.0"
|
||||||
"@codemirror/language": "^6.0.0",
|
|
||||||
"@codemirror/lang-javascript": "^6.0.0",
|
|
||||||
"@codemirror/lang-json": "^6.0.0",
|
|
||||||
"@codemirror/lang-python": "^6.0.0",
|
|
||||||
"@codemirror/lang-markdown": "^6.0.0",
|
|
||||||
"@codemirror/autocomplete": "^6.0.0",
|
|
||||||
"@codemirror/theme-one-dark": "^6.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vite": "^6.0.0",
|
"vite": "^6.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"vue-tsc": "^2.0.0"
|
||||||
"vue-tsc": "^2.0.0",
|
|
||||||
"tailwindcss": "^4.0.0",
|
|
||||||
"@tailwindcss/vite": "^4.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"core:window:allow-create",
|
"core:window:allow-create",
|
||||||
"core:webview:default",
|
"core:webview:default",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
"shell:allow-open"
|
"shell:allow-open",
|
||||||
|
"updater:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Default capabilities for the main Wraith window","local":true,"windows":["main","tool-*"],"permissions":["core:default","core:event:default","core:window:default","core:window:allow-create","core:webview:default","core:webview:allow-create-webview-window","shell:allow-open"]}}
|
{"default":{"identifier":"default","description":"Default capabilities for the main Wraith window","local":true,"windows":["main","tool-*"],"permissions":["core:default","core:event:default","core:window:default","core:window:allow-create","core:webview:default","core:webview:allow-create-webview-window","shell:allow-open","updater:default"]}}
|
||||||
@ -92,3 +92,19 @@ pub fn search_connections(
|
|||||||
) -> Result<Vec<ConnectionRecord>, String> {
|
) -> Result<Vec<ConnectionRecord>, String> {
|
||||||
state.connections.search(&query)
|
state.connections.search(&query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn reorder_connections(
|
||||||
|
ids: Vec<i64>,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.connections.reorder_connections(&ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn reorder_groups(
|
||||||
|
ids: Vec<i64>,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.connections.reorder_groups(&ids)
|
||||||
|
}
|
||||||
|
|||||||
@ -429,6 +429,32 @@ impl ConnectionService {
|
|||||||
|
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Batch-update sort_order for a list of connection IDs.
|
||||||
|
pub fn reorder_connections(&self, ids: &[i64]) -> Result<(), String> {
|
||||||
|
let conn = self.db.conn();
|
||||||
|
for (i, id) in ids.iter().enumerate() {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE connections SET sort_order = ?1 WHERE id = ?2",
|
||||||
|
params![i as i64, id],
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to reorder connection {id}: {e}"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Batch-update sort_order for a list of group IDs.
|
||||||
|
pub fn reorder_groups(&self, ids: &[i64]) -> Result<(), String> {
|
||||||
|
let conn = self.db.conn();
|
||||||
|
for (i, id) in ids.iter().enumerate() {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE groups SET sort_order = ?1 WHERE id = ?2",
|
||||||
|
params![i as i64, id],
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to reorder group {id}: {e}"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── private helpers ───────────────────────────────────────────────────────────
|
// ── private helpers ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -130,6 +130,7 @@ pub fn run() {
|
|||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.manage(app_state)
|
.manage(app_state)
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@ -191,7 +192,7 @@ pub fn run() {
|
|||||||
commands::vault::is_first_run, commands::vault::create_vault, commands::vault::unlock, commands::vault::is_unlocked,
|
commands::vault::is_first_run, commands::vault::create_vault, commands::vault::unlock, commands::vault::is_unlocked,
|
||||||
commands::settings::get_setting, commands::settings::set_setting,
|
commands::settings::get_setting, commands::settings::set_setting,
|
||||||
commands::connections::list_connections, commands::connections::create_connection, commands::connections::get_connection, commands::connections::update_connection, commands::connections::delete_connection,
|
commands::connections::list_connections, commands::connections::create_connection, commands::connections::get_connection, commands::connections::update_connection, commands::connections::delete_connection,
|
||||||
commands::connections::list_groups, commands::connections::create_group, commands::connections::delete_group, commands::connections::rename_group, commands::connections::search_connections,
|
commands::connections::list_groups, commands::connections::create_group, commands::connections::delete_group, commands::connections::rename_group, commands::connections::search_connections, commands::connections::reorder_connections, commands::connections::reorder_groups,
|
||||||
commands::credentials::list_credentials, commands::credentials::create_password, commands::credentials::create_ssh_key, commands::credentials::delete_credential, commands::credentials::decrypt_password, commands::credentials::decrypt_ssh_key,
|
commands::credentials::list_credentials, commands::credentials::create_password, commands::credentials::create_ssh_key, commands::credentials::delete_credential, commands::credentials::decrypt_password, commands::credentials::decrypt_ssh_key,
|
||||||
commands::ssh_commands::connect_ssh, commands::ssh_commands::connect_ssh_with_key, commands::ssh_commands::ssh_write, commands::ssh_commands::ssh_resize, commands::ssh_commands::disconnect_ssh, commands::ssh_commands::disconnect_session, commands::ssh_commands::list_ssh_sessions,
|
commands::ssh_commands::connect_ssh, commands::ssh_commands::connect_ssh_with_key, commands::ssh_commands::ssh_write, commands::ssh_commands::ssh_resize, commands::ssh_commands::disconnect_ssh, commands::ssh_commands::disconnect_session, commands::ssh_commands::list_ssh_sessions,
|
||||||
commands::sftp_commands::sftp_list, commands::sftp_commands::sftp_read_file, commands::sftp_commands::sftp_write_file, commands::sftp_commands::sftp_mkdir, commands::sftp_commands::sftp_delete, commands::sftp_commands::sftp_rename,
|
commands::sftp_commands::sftp_list, commands::sftp_commands::sftp_read_file, commands::sftp_commands::sftp_write_file, commands::sftp_commands::sftp_mkdir, commands::sftp_commands::sftp_delete, commands::sftp_commands::sftp_rename,
|
||||||
|
|||||||
@ -48,6 +48,12 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNCRkQ2OUY2OEY0Q0ZFQkYKUldTLy9reVA5bW45T3dUQ1R5OFNCenVhL2srTXlLcHR4cFNaeCtJSmJUSTZKSUNHVTRIbWZwanEK",
|
||||||
|
"endpoints": [
|
||||||
|
"https://git.command.vigilcyber.com/api/packages/vstockwell/generic/wraith/latest/update.json"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -320,7 +320,25 @@ async function checkUpdates(): Promise<void> {
|
|||||||
updateChecking.value = true;
|
updateChecking.value = true;
|
||||||
updateInfo.value = null;
|
updateInfo.value = null;
|
||||||
try {
|
try {
|
||||||
updateInfo.value = await invoke<UpdateCheckInfo>("check_for_updates");
|
const { check } = await import("@tauri-apps/plugin-updater");
|
||||||
|
const update = await check();
|
||||||
|
if (update?.available) {
|
||||||
|
updateInfo.value = {
|
||||||
|
currentVersion: await getVersion(),
|
||||||
|
latestVersion: update.version || "unknown",
|
||||||
|
updateAvailable: true,
|
||||||
|
downloadUrl: "",
|
||||||
|
releaseNotes: update.body || "",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updateInfo.value = {
|
||||||
|
currentVersion: await getVersion(),
|
||||||
|
latestVersion: await getVersion(),
|
||||||
|
updateAvailable: false,
|
||||||
|
downloadUrl: "",
|
||||||
|
releaseNotes: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Update check failed: ${err}`);
|
alert(`Update check failed: ${err}`);
|
||||||
}
|
}
|
||||||
@ -328,11 +346,16 @@ async function checkUpdates(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadUpdate(): Promise<void> {
|
async function downloadUpdate(): Promise<void> {
|
||||||
if (!updateInfo.value?.downloadUrl) return;
|
|
||||||
try {
|
try {
|
||||||
await shellOpen(updateInfo.value.downloadUrl);
|
const { check } = await import("@tauri-apps/plugin-updater");
|
||||||
} catch {
|
const update = await check();
|
||||||
window.open(updateInfo.value.downloadUrl, "_blank");
|
if (update?.available) {
|
||||||
|
await update.downloadAndInstall();
|
||||||
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||||
|
await relaunch();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Update failed: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const currentVersion = ref("loading...");
|
const currentVersion = ref("loading...");
|
||||||
|
|||||||
@ -157,26 +157,22 @@ function onGroupDragOver(target: Group): void {
|
|||||||
|
|
||||||
async function onGroupDrop(target: Group): Promise<void> {
|
async function onGroupDrop(target: Group): Promise<void> {
|
||||||
if (draggedGroup && draggedGroup.id !== target.id) {
|
if (draggedGroup && draggedGroup.id !== target.id) {
|
||||||
// Reorder groups — swap sort_order
|
|
||||||
const groups = connectionStore.groups;
|
const groups = connectionStore.groups;
|
||||||
const fromIdx = groups.findIndex(g => g.id === draggedGroup!.id);
|
const fromIdx = groups.findIndex(g => g.id === draggedGroup!.id);
|
||||||
const toIdx = groups.findIndex(g => g.id === target.id);
|
const toIdx = groups.findIndex(g => g.id === target.id);
|
||||||
if (fromIdx !== -1 && toIdx !== -1) {
|
if (fromIdx !== -1 && toIdx !== -1) {
|
||||||
const [moved] = groups.splice(fromIdx, 1);
|
const [moved] = groups.splice(fromIdx, 1);
|
||||||
groups.splice(toIdx, 0, moved);
|
groups.splice(toIdx, 0, moved);
|
||||||
|
// Persist new order
|
||||||
|
const ids = groups.map(g => g.id);
|
||||||
|
invoke("reorder_groups", { ids }).catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (draggedConn && draggedConn.fromGroupId !== target.id) {
|
if (draggedConn && draggedConn.fromGroupId !== target.id) {
|
||||||
// Move connection to different group
|
|
||||||
try {
|
try {
|
||||||
await invoke("update_connection", {
|
await invoke("update_connection", { id: draggedConn.conn.id, input: { groupId: target.id } });
|
||||||
id: draggedConn.conn.id,
|
|
||||||
input: { groupId: target.id },
|
|
||||||
});
|
|
||||||
await connectionStore.loadAll();
|
await connectionStore.loadAll();
|
||||||
} catch (err) {
|
} catch (err) { console.error("Failed to move connection:", err); }
|
||||||
console.error("Failed to move connection:", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
resetDragState();
|
resetDragState();
|
||||||
}
|
}
|
||||||
@ -195,26 +191,21 @@ function onConnDragOver(target: Connection): void {
|
|||||||
|
|
||||||
async function onConnDrop(target: Connection, targetGroupId: number): Promise<void> {
|
async function onConnDrop(target: Connection, targetGroupId: number): Promise<void> {
|
||||||
if (draggedConn && draggedConn.conn.id !== target.id) {
|
if (draggedConn && draggedConn.conn.id !== target.id) {
|
||||||
// Reorder within group or move between groups
|
|
||||||
if (draggedConn.fromGroupId !== targetGroupId) {
|
if (draggedConn.fromGroupId !== targetGroupId) {
|
||||||
// Move to different group
|
|
||||||
try {
|
try {
|
||||||
await invoke("update_connection", {
|
await invoke("update_connection", { id: draggedConn.conn.id, input: { groupId: targetGroupId } });
|
||||||
id: draggedConn.conn.id,
|
|
||||||
input: { groupId: targetGroupId },
|
|
||||||
});
|
|
||||||
await connectionStore.loadAll();
|
await connectionStore.loadAll();
|
||||||
} catch (err) {
|
} catch (err) { console.error("Failed to move connection:", err); }
|
||||||
console.error("Failed to move connection:", err);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Reorder within same group
|
|
||||||
const conns = connectionStore.connectionsByGroup(targetGroupId);
|
const conns = connectionStore.connectionsByGroup(targetGroupId);
|
||||||
const fromIdx = conns.findIndex(c => c.id === draggedConn!.conn.id);
|
const fromIdx = conns.findIndex(c => c.id === draggedConn!.conn.id);
|
||||||
const toIdx = conns.findIndex(c => c.id === target.id);
|
const toIdx = conns.findIndex(c => c.id === target.id);
|
||||||
if (fromIdx !== -1 && toIdx !== -1) {
|
if (fromIdx !== -1 && toIdx !== -1) {
|
||||||
const [moved] = conns.splice(fromIdx, 1);
|
const [moved] = conns.splice(fromIdx, 1);
|
||||||
conns.splice(toIdx, 0, moved);
|
conns.splice(toIdx, 0, moved);
|
||||||
|
// Persist new order
|
||||||
|
const ids = conns.map(c => c.id);
|
||||||
|
invoke("reorder_connections", { ids }).catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -462,16 +462,19 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Check for updates on startup (non-blocking)
|
// Check for updates on startup via Tauri updater plugin (non-blocking)
|
||||||
invoke<{ currentVersion: string; latestVersion: string; updateAvailable: boolean; downloadUrl: string }>("check_for_updates")
|
import("@tauri-apps/plugin-updater").then(async ({ check }) => {
|
||||||
.then((info) => {
|
try {
|
||||||
if (info.updateAvailable) {
|
const update = await check();
|
||||||
if (confirm(`Wraith v${info.latestVersion} is available (you have v${info.currentVersion}). Open download page?`)) {
|
if (update?.available) {
|
||||||
import("@tauri-apps/plugin-shell").then(({ open }) => open(info.downloadUrl)).catch(() => window.open(info.downloadUrl, "_blank"));
|
if (confirm(`Wraith v${update.version} is available. Download and install?`)) {
|
||||||
|
await update.downloadAndInstall();
|
||||||
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||||
|
await relaunch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
} catch {}
|
||||||
.catch(() => {}); // Silent fail — no internet is fine
|
}).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@ -240,8 +240,23 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
let password = "";
|
let password = "";
|
||||||
let domain = "";
|
let domain = "";
|
||||||
|
|
||||||
// Extract stored credentials from connection options JSON if present
|
// Try vault credentials first (same as SSH path)
|
||||||
if (conn.options) {
|
if (conn.credentialId) {
|
||||||
|
try {
|
||||||
|
const allCreds = await invoke<{ id: number; name: string; username: string | null; domain: string | null; credentialType: string; sshKeyId: number | null }[]>("list_credentials");
|
||||||
|
const cred = allCreds.find((c) => c.id === conn.credentialId);
|
||||||
|
if (cred && cred.credentialType === "password") {
|
||||||
|
username = cred.username ?? "";
|
||||||
|
domain = cred.domain ?? "";
|
||||||
|
password = await invoke<string>("decrypt_password", { credentialId: cred.id });
|
||||||
|
}
|
||||||
|
} catch (credErr) {
|
||||||
|
console.warn("Failed to resolve RDP credential from vault:", credErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to connection options JSON if vault didn't provide creds
|
||||||
|
if (!username && conn.options) {
|
||||||
try {
|
try {
|
||||||
const opts = JSON.parse(conn.options);
|
const opts = JSON.parse(conn.options);
|
||||||
if (opts?.username) username = opts.username;
|
if (opts?.username) username = opts.username;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user