initial ssh key model and creation ui
This commit is contained in:
parent
f311fde74e
commit
5e6542d08e
@ -56,6 +56,7 @@ pub fn run() -> tauri::Result<()> {
|
|||||||
ipc::save_credential,
|
ipc::save_credential,
|
||||||
ipc::delete_credential,
|
ipc::delete_credential,
|
||||||
ipc::list_credentials,
|
ipc::list_credentials,
|
||||||
|
ipc::sshkey_from_file,
|
||||||
ipc::get_config,
|
ipc::get_config,
|
||||||
ipc::save_config,
|
ipc::save_config,
|
||||||
ipc::launch_terminal,
|
ipc::launch_terminal,
|
||||||
|
8
src-tauri/src/credentials/fixtures/ssh_ed25519_enc
Normal file
8
src-tauri/src/credentials/fixtures/ssh_ed25519_enc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAWtYanP1
|
||||||
|
TBKT8lBL4IzKpYAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIASRxkrR1+nCDZiZ
|
||||||
|
N2d8Muvl+zW8undCJVCo+qVMAjkjAAAAkI021XFPzB9VnO8uGAQ8f3bwP/ki5fDVuWD7Fc
|
||||||
|
crN+yfT8Ugjhc7IL2dIt/xj9iJIa9fJDw0pg1Y8issqp9C8HVhasyWpf2iwJIalUHTOekn
|
||||||
|
WdoxA+/OQBstRBKSv43sI801+9OC8dXCMNM2QzpiGNs0QxdLJpcJQhHEvqq/yDIODF0p7M
|
||||||
|
h3e9eYGVPOR0CjlQ==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
1
src-tauri/src/credentials/fixtures/ssh_ed25519_enc.pub
Normal file
1
src-tauri/src/credentials/fixtures/ssh_ed25519_enc.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIASRxkrR1+nCDZiZN2d8Muvl+zW8undCJVCo+qVMAjkj hello world
|
7
src-tauri/src/credentials/fixtures/ssh_ed25519_plain
Normal file
7
src-tauri/src/credentials/fixtures/ssh_ed25519_plain
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACC7sFhGkIp/SBnKab5Q6UZY/W9R0k/+ztZ4Vm1D4d1r8gAAAJAwEcgHMBHI
|
||||||
|
BwAAAAtzc2gtZWQyNTUxOQAAACC7sFhGkIp/SBnKab5Q6UZY/W9R0k/+ztZ4Vm1D4d1r8g
|
||||||
|
AAAEB9VXgjePmpl6Q3Y1t2a4DZhsdRf+183vWAJWAonDOneLuwWEaQin9IGcppvlDpRlj9
|
||||||
|
b1HST/7O1nhWbUPh3WvyAAAAC2hlbGxvIHdvcmxkAQI=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
@ -0,0 +1 @@
|
|||||||
|
{"path":"./src/credentials/fixtures/ssh_ed25519_plain","algorithm":"ssh-ed25519","comment":"hello world","public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILuwWEaQin9IGcppvlDpRlj9b1HST/7O1nhWbUPh3Wvy hello world","private_key":"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACC7sFhGkIp/SBnKab5Q6UZY/W9R0k/+ztZ4Vm1D4d1r8gAAAJAwEcgHMBHI\nBwAAAAtzc2gtZWQyNTUxOQAAACC7sFhGkIp/SBnKab5Q6UZY/W9R0k/+ztZ4Vm1D4d1r8g\nAAAEB9VXgjePmpl6Q3Y1t2a4DZhsdRf+183vWAJWAonDOneLuwWEaQin9IGcppvlDpRlj9\nb1HST/7O1nhWbUPh3WvyAAAAC2hlbGxvIHdvcmxkAQI=\n-----END OPENSSH PRIVATE KEY-----\n"}
|
1
src-tauri/src/credentials/fixtures/ssh_ed25519_plain.pub
Normal file
1
src-tauri/src/credentials/fixtures/ssh_ed25519_plain.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILuwWEaQin9IGcppvlDpRlj9b1HST/7O1nhWbUPh3Wvy hello world
|
39
src-tauri/src/credentials/fixtures/ssh_rsa_enc
Normal file
39
src-tauri/src/credentials/fixtures/ssh_rsa_enc
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
bnzyMMiKRuZb16OucNli84FIAuI=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
1
src-tauri/src/credentials/fixtures/ssh_rsa_enc.pub
Normal file
1
src-tauri/src/credentials/fixtures/ssh_rsa_enc.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCwIeD+SUIx511M/JztbfUkEi8OhnF3ELsGYjbRq/ABy0x8tYlk6ZjlOFg2kSMAEpoTNOVJp+9eDsQRXZfgmQOKz7ugrS/l1XT38/8SKpe1n3XYti3Nkh/xpbv9y1XXd3mkGr1GUorviywNqWNw/Ox5OH7tasHAztBBrpecu4gL7GwXkXcRFD8cTQNVSNJzdz0B4/ZDRjgRtzOdn0s/yNH+z3YciHjBNeLmANnSMPEaOtjgQV0akjo5jRCOkEP2MKm3uxMQknyopGRVCW4aJyula28G/uV2TjyKyF6l3kCK7Y7FSb50n7IxwaLMldoANdsAmp37LGIoM6VM/Muf+xcxWQZfMzXG3a+7Uqgs1cMnGYxJbCpEBPGlRNghdfkVlUSSpEiJVLN8eMH4G0Z6BflszCYUbN9RevcWdARpR7Ec6AsOJ3squiORWvEemp+dBXF+Hw7XA0H0cAhVafiNj1y7gXlgWgv4hTeleJMynRXx+Mo1gr42EkEPBlaFM/gBYC8= hello world
|
38
src-tauri/src/credentials/fixtures/ssh_rsa_plain
Normal file
38
src-tauri/src/credentials/fixtures/ssh_rsa_plain
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
PkAC2GY5AmhSkAAAALaGVsbG8gd29ybGQ=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
1
src-tauri/src/credentials/fixtures/ssh_rsa_plain.pub
Normal file
1
src-tauri/src/credentials/fixtures/ssh_rsa_plain.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDEq85taUAJEuutUnczQB4w6/PclDO3nI40PXr74ZqfMJk0giV32YBzRrSNT7BgTQItqCblYkY15M4ZhRql0w39IAfembBK5MLwCCPf/DyN3mLwdIMcH4kDBnyD3M19nO6GQ8k8UpH2tQR/U2RqN8hAmJNP/eWIK13XaWzdxEIcOeKJR2jP1mUM5YXjWj9zmwFWUKpGmr3vxph+Vdr+x9QLQ4hlTtMgXRhSjYgZJ8QsviEMz29JqQYZrW5qy/F2jX7FL/nLhb5ge5QUlhVmKSAWh1FkwcHR+9TDVp1EJKfxnQQ6vN7lBXPfxPx/LCcYqnZSj6ImwN1VMNxwXDCQHhveiP5cw1yuWriCbR5/lw2+0KD36YM8/HMjofEyNSjVzDwArrmBZdZ3yvZL1pcpEyJk2XG1xJHQrq9TqtItA3VrLkN1RQLoRIgRfuu5YszfXfWWgsHpukctWrm4PbKGLn6jgOj9IN6TaMq8u8XJXCM6Ut5d/l6Ry1kBnQC1KccMQwU= hello world
|
@ -14,14 +14,17 @@ use crate::errors::*;
|
|||||||
mod aws;
|
mod aws;
|
||||||
pub use aws::{AwsBaseCredential, AwsSessionCredential};
|
pub use aws::{AwsBaseCredential, AwsSessionCredential};
|
||||||
|
|
||||||
|
mod crypto;
|
||||||
|
pub use crypto::Crypto;
|
||||||
|
|
||||||
mod record;
|
mod record;
|
||||||
pub use record::CredentialRecord;
|
pub use record::CredentialRecord;
|
||||||
|
|
||||||
mod session;
|
mod session;
|
||||||
pub use session::AppSession;
|
pub use session::AppSession;
|
||||||
|
|
||||||
mod crypto;
|
mod ssh;
|
||||||
pub use crypto::Crypto;
|
pub use ssh::SshKey;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
120
src-tauri/src/credentials/ssh.rs
Normal file
120
src-tauri/src/credentials/ssh.rs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{
|
||||||
|
Deserialize,
|
||||||
|
Serialize,
|
||||||
|
Serializer,
|
||||||
|
ser::Error,
|
||||||
|
ser::SerializeStruct,
|
||||||
|
};
|
||||||
|
use ssh_key::{
|
||||||
|
Algorithm,
|
||||||
|
LineEnding,
|
||||||
|
private::PrivateKey,
|
||||||
|
public::PublicKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct SshKey {
|
||||||
|
pub path: String,
|
||||||
|
pub algorithm: Algorithm,
|
||||||
|
pub comment: String,
|
||||||
|
pub public_key: PublicKey,
|
||||||
|
pub private_key: PrivateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshKey {
|
||||||
|
pub fn from_file(path: String, passphrase: &str) -> Result<SshKey, LoadSshKeyError> {
|
||||||
|
let mut privkey = PrivateKey::read_openssh_file(path.as_ref())?;
|
||||||
|
if privkey.is_encrypted() {
|
||||||
|
privkey = privkey.decrypt(passphrase)
|
||||||
|
.map_err(|_| LoadSshKeyError::InvalidPassphrase)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SshKey {
|
||||||
|
path,
|
||||||
|
algorithm: privkey.algorithm(),
|
||||||
|
comment: privkey.comment().into(),
|
||||||
|
public_key: privkey.public_key().clone(),
|
||||||
|
private_key: privkey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Serialize for SshKey {
|
||||||
|
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut key = s.serialize_struct("SshKey", 5)?;
|
||||||
|
key.serialize_field("path", &self.path)?;
|
||||||
|
key.serialize_field("algorithm", self.algorithm.as_str())?;
|
||||||
|
key.serialize_field("comment", &self.comment)?;
|
||||||
|
|
||||||
|
let pubkey_str = self.public_key.to_openssh()
|
||||||
|
.map_err(|e| S::Error::custom(format!("Failed to encode SSH public key: {e}")))?;
|
||||||
|
key.serialize_field("public_key", &pubkey_str)?;
|
||||||
|
|
||||||
|
let privkey_str = self.private_key.to_openssh(LineEnding::LF)
|
||||||
|
.map_err(|e| S::Error::custom(format!("Failed to encode SSH private key: {e}")))?;
|
||||||
|
key.serialize_field::<str>("private_key", privkey_str.as_ref())?;
|
||||||
|
|
||||||
|
key.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn path(name: &str) -> String {
|
||||||
|
format!("./src/credentials/fixtures/{name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_rsa_plain() {
|
||||||
|
let k = SshKey::from_file(path("ssh_rsa_plain"), "")
|
||||||
|
.expect("Failed to load SSH key");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_rsa_enc() {
|
||||||
|
let k = SshKey::from_file(
|
||||||
|
path("ssh_rsa_enc"),
|
||||||
|
"correct horse battery staple",
|
||||||
|
).expect("Failed to load SSH key");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_ed25519_plain() {
|
||||||
|
let k = SshKey::from_file(path("ssh_ed25519_plain"), "")
|
||||||
|
.expect("Failed to load SSH key");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_ed25519_enc() {
|
||||||
|
let k = SshKey::from_file(
|
||||||
|
path("ssh_ed25519_enc"),
|
||||||
|
"correct horse battery staple",
|
||||||
|
).expect("Failed to load SSH key");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize() {
|
||||||
|
let expected = fs::read_to_string(path("ssh_ed25519_plain.json")).unwrap();
|
||||||
|
|
||||||
|
let k = SshKey::from_file(path("ssh_ed25519_plain"), "").unwrap();
|
||||||
|
let computed = serde_json::to_string(&k)
|
||||||
|
.expect("Failed to serialize SshKey");
|
||||||
|
|
||||||
|
assert_eq!(expected, computed);
|
||||||
|
}
|
||||||
|
}
|
@ -410,6 +410,17 @@ pub enum LaunchTerminalError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum LoadSshKeyError {
|
||||||
|
#[error("Passphrase is invalid")]
|
||||||
|
InvalidPassphrase,
|
||||||
|
#[error("Could not parse SSH private key data")]
|
||||||
|
InvalidData(#[from] ssh_key::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Serialize implementations
|
// Serialize implementations
|
||||||
// =========================
|
// =========================
|
||||||
@ -436,6 +447,7 @@ impl_serialize_basic!(WindowError);
|
|||||||
impl_serialize_basic!(LockError);
|
impl_serialize_basic!(LockError);
|
||||||
impl_serialize_basic!(SaveCredentialsError);
|
impl_serialize_basic!(SaveCredentialsError);
|
||||||
impl_serialize_basic!(LoadCredentialsError);
|
impl_serialize_basic!(LoadCredentialsError);
|
||||||
|
impl_serialize_basic!(LoadSshKeyError);
|
||||||
|
|
||||||
|
|
||||||
impl Serialize for HandlerError {
|
impl Serialize for HandlerError {
|
||||||
|
@ -5,7 +5,8 @@ use tauri::{AppHandle, State};
|
|||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::credentials::{
|
use crate::credentials::{
|
||||||
AppSession,
|
AppSession,
|
||||||
CredentialRecord
|
CredentialRecord,
|
||||||
|
SshKey,
|
||||||
};
|
};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::clientinfo::Client;
|
use crate::clientinfo::Client;
|
||||||
@ -134,6 +135,12 @@ pub async fn list_credentials(app_state: State<'_, AppState>) -> Result<Vec<Cred
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn sshkey_from_file(path: String, passphrase: &str) -> Result<SshKey, LoadSshKeyError> {
|
||||||
|
SshKey::from_file(path, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
|
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
|
||||||
let config = app_state.config.read().await;
|
let config = app_state.config.read().await;
|
||||||
|
39
src/ui/FileInput.svelte
Normal file
39
src/ui/FileInput.svelte
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
// import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
|
|
||||||
|
import Icon from './Icon.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
export let value = {};
|
||||||
|
export let params = {};
|
||||||
|
|
||||||
|
async function chooseFile() {
|
||||||
|
let file = await open(params);
|
||||||
|
if (file) {
|
||||||
|
value = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// some day, figure out drag-and-drop
|
||||||
|
// let drag = null;
|
||||||
|
// listen('tauri://drag', e => drag = e);
|
||||||
|
// listen('tauri://drop', e => console.log(e));
|
||||||
|
// listen('tauri://drag-cancelled', e => console.log(e));
|
||||||
|
// listen('tauri://drop-over', e => console.log(e));
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="relative flex join has-[:focus]:outline outline-2 outline-offset-2 outline-base-content/20">
|
||||||
|
<button type="button" class="btn btn-neutral join-item" on:click={chooseFile}>
|
||||||
|
Choose file
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="join-item grow input input-bordered border-l-0 bg-transparent focus:outline-none"
|
||||||
|
value={value?.name || ''}
|
||||||
|
on:input={e => value.path = e.target.value}
|
||||||
|
on:change on:input on:focus on:blur
|
||||||
|
>
|
||||||
|
</div>
|
@ -19,13 +19,13 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<div class="join w-full">
|
<div class="join w-full has-[:focus]:outline outline-2 outline-offset-2 outline-base-content/20">
|
||||||
<input
|
<input
|
||||||
type={show ? 'text' : 'password'}
|
type={show ? 'text' : 'password'}
|
||||||
{value} {placeholder} {autofocus}
|
{value} {placeholder} {autofocus}
|
||||||
on:input={e => value = e.target.value}
|
on:input={e => value = e.target.value}
|
||||||
on:input on:change on:focus on:blur
|
on:input on:change on:focus on:blur
|
||||||
class="input input-bordered flex-grow join-item placeholder:text-gray-500 {classes}"
|
class="input input-bordered flex-grow join-item placeholder:text-gray-500 focus:outline-none {classes}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -5,12 +5,16 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
import AwsCredential from './credentials/AwsCredential.svelte';
|
import AwsCredential from './credentials/AwsCredential.svelte';
|
||||||
|
import NewSshKey from './credentials/NewSshKey.svelte';
|
||||||
import Icon from '../ui/Icon.svelte';
|
import Icon from '../ui/Icon.svelte';
|
||||||
import Nav from '../ui/Nav.svelte';
|
import Nav from '../ui/Nav.svelte';
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
|
|
||||||
let records = []
|
let records = null
|
||||||
|
$: awsRecords = (records || []).filter(r => r.credential.type === 'AwsBase');
|
||||||
|
$: sshRecords = (records || []).filter(r => r.credential.type === 'SshKey');
|
||||||
|
|
||||||
let defaults = writable({});
|
let defaults = writable({});
|
||||||
async function loadCreds() {
|
async function loadCreds() {
|
||||||
records = await invoke('list_credentials');
|
records = await invoke('list_credentials');
|
||||||
@ -24,7 +28,18 @@
|
|||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: null,
|
name: null,
|
||||||
is_default: false,
|
is_default: false,
|
||||||
credential: {type: 'AwsBase', AccessKeyId: null, SecretAccessKey: null},
|
credential: {type: 'AwsBase', AccessKeyId: '', SecretAccessKey: ''},
|
||||||
|
isNew: true,
|
||||||
|
});
|
||||||
|
records = records;
|
||||||
|
}
|
||||||
|
|
||||||
|
function newSsh() {
|
||||||
|
records.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: null,
|
||||||
|
is_default: false,
|
||||||
|
credential: {type: 'SshKey', algorithm: '', private_key: '', public_key: '', comment: '',},
|
||||||
isNew: true,
|
isNew: true,
|
||||||
});
|
});
|
||||||
records = records;
|
records = records;
|
||||||
@ -36,27 +51,53 @@
|
|||||||
<h1 slot="title" class="text-2xl font-bold">Credentials</h1>
|
<h1 slot="title" class="text-2xl font-bold">Credentials</h1>
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
<div class="max-w-xl mx-auto mb-12 flex flex-col gap-y-4 justify-center">
|
<div class="max-w-xl mx-auto mb-12 flex flex-col gap-y-12 justify-center">
|
||||||
<div class="divider">
|
<div class="flex flex-col gap-y-4">
|
||||||
<h2 class="text-xl font-bold">AWS Access Keys</h2>
|
<div class="divider">
|
||||||
</div>
|
<h2 class="text-xl font-bold">AWS Access Keys</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if records.length > 0}
|
{#if awsRecords.length > 0}
|
||||||
{#each records as record (record.id)}
|
{#each awsRecords as record (record.id)}
|
||||||
<AwsCredential {record} {defaults} on:update={loadCreds} />
|
<AwsCredential {record} {defaults} on:update={loadCreds} />
|
||||||
{/each}
|
{/each}
|
||||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
|
||||||
<Icon name="plus-circle-mini" class="size-5" />
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
|
|
||||||
<div>You have no saved AWS credentials.</div>
|
|
||||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||||
<Icon name="plus-circle-mini" class="size-5" />
|
<Icon name="plus-circle-mini" class="size-5" />
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
|
{:else if records !== null}
|
||||||
|
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
|
||||||
|
<div>You have no saved AWS credentials.</div>
|
||||||
|
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||||
|
<Icon name="plus-circle-mini" class="size-5" />
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-y-4">
|
||||||
|
<div class="divider">
|
||||||
|
<h2 class="text-xl font-bold">SSH Keys</h2>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
{#if sshRecords.length > 0}
|
||||||
|
{#each sshRecords as record (record.id)}
|
||||||
|
{#if record.isNew}
|
||||||
|
<NewSshKey {record} on:update={e => console.log(e)} />
|
||||||
|
{:else}
|
||||||
|
<!-- EditSshKey -->
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{:else if records !== null}
|
||||||
|
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
|
||||||
|
<div>You have no saved SSH keys.</div>
|
||||||
|
<button class="btn btn-primary btn-wide mx-auto" on:click={newSsh}>
|
||||||
|
<Icon name="plus-circle-mini" class="size-5" />
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
let showDetails = record.isNew ? true : false;
|
let showDetails = record.isNew ? true : false;
|
||||||
|
|
||||||
let localName = name;
|
|
||||||
let local = JSON.parse(JSON.stringify(record));
|
let local = JSON.parse(JSON.stringify(record));
|
||||||
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
||||||
|
|
||||||
@ -63,7 +62,13 @@
|
|||||||
class="rounded-box space-y-4 bg-base-200 {record.is_default ? 'border border-accent' : ''}"
|
class="rounded-box space-y-4 bg-base-200 {record.is_default ? 'border border-accent' : ''}"
|
||||||
>
|
>
|
||||||
<div class="flex items-center px-6 py-4 gap-x-4">
|
<div class="flex items-center px-6 py-4 gap-x-4">
|
||||||
<h3 class="text-lg font-bold">{record.name || ''}</h3>
|
<h3 class="text-lg font-bold">
|
||||||
|
{#if !record?.isNew && showDetails}
|
||||||
|
<input type="text" class="input input-bordered bg-transparent" bind:value={local.name}>
|
||||||
|
{:else}
|
||||||
|
{record.name || ''}
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
|
||||||
{#if record.is_default}
|
{#if record.is_default}
|
||||||
<span class="badge badge-accent">Default</span>
|
<span class="badge badge-accent">Default</span>
|
||||||
@ -129,9 +134,9 @@
|
|||||||
transition:fade={{duration: 100}}
|
transition:fade={{duration: 100}}
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
92
src/views/credentials/NewSshKey.svelte
Normal file
92
src/views/credentials/NewSshKey.svelte
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { homeDir } from '@tauri-apps/api/path';
|
||||||
|
import { fade, slide } from 'svelte/transition';
|
||||||
|
|
||||||
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
|
import FileInput from '../../ui/FileInput.svelte';
|
||||||
|
import Icon from '../../ui/Icon.svelte';
|
||||||
|
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
|
||||||
|
let name;
|
||||||
|
let file;
|
||||||
|
let passphrase = '';
|
||||||
|
let showDetails = true;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let defaultPath = null;
|
||||||
|
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||||
|
|
||||||
|
let alert;
|
||||||
|
async function saveCredential() {
|
||||||
|
let key = await invoke('sshkey_from_file', {path: file.path, passphrase});
|
||||||
|
dispatch('update', key);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
transition:slide|local={{duration: 300}}
|
||||||
|
class="rounded-box space-y-4 bg-base-200"
|
||||||
|
>
|
||||||
|
<div class="flex justify-end px-6 py-4 gap-x-4">
|
||||||
|
<div class="join ml-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline join-item"
|
||||||
|
on:click={() => showDetails = !showDetails}
|
||||||
|
>
|
||||||
|
<Icon name="pencil" class="size-6" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline btn-error join-item"
|
||||||
|
on:click={() => dispatch('update')}
|
||||||
|
>
|
||||||
|
<Icon name="trash" class="size-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showDetails}
|
||||||
|
<form
|
||||||
|
transition:slide|local={{duration: 200}}
|
||||||
|
class=" px-6 pb-4 space-y-4"
|
||||||
|
on:submit|preventDefault={() => alert.run(saveCredential)}
|
||||||
|
>
|
||||||
|
<ErrorAlert bind:this={alert} />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
||||||
|
<span class="justify-self-end">Name</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered bg-transparent"
|
||||||
|
vind:value={name}
|
||||||
|
>
|
||||||
|
|
||||||
|
<span class="justify-self-end">File</span>
|
||||||
|
<FileInput bind:value={file} params={{defaultPath}} />
|
||||||
|
|
||||||
|
<span class="justify-self-end">Passphrase</span>
|
||||||
|
<PassphraseInput class="bg-transparent" bind:value={passphrase} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
{#if file?.path}
|
||||||
|
<button
|
||||||
|
transition:fade={{duration: 100}}
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
110
src/views/credentials/SshKey.svelte
Normal file
110
src/views/credentials/SshKey.svelte
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script>
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { homeDir } from '@tauri-apps/api/path';
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import { fade, slide } from 'svelte/transition';
|
||||||
|
|
||||||
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
|
import FileInput from '../../ui/FileInput.svelte';
|
||||||
|
import Icon from '../../ui/Icon.svelte';
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
|
||||||
|
let showDetails = record.isNew ? true : false;
|
||||||
|
|
||||||
|
let local = JSON.parse(JSON.stringify(record));
|
||||||
|
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
||||||
|
|
||||||
|
let file;
|
||||||
|
let passphrase;
|
||||||
|
|
||||||
|
let defaultPath = null;
|
||||||
|
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||||
|
|
||||||
|
function conditionalDelete() {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
|
||||||
|
let alert;
|
||||||
|
async function saveCredential() {
|
||||||
|
let key = await invoke('sshkey_from_file', {startDir, passphrase});
|
||||||
|
record.credential = {type: 'SshKey', ...key};
|
||||||
|
record.isNew = false; // just for now
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
transition:slide|local={{duration: record.isNew ? 300 : 0}}
|
||||||
|
class="rounded-box space-y-4 bg-base-200"
|
||||||
|
>
|
||||||
|
<div class="flex items-center px-6 py-4 gap-x-4">
|
||||||
|
<h3 class="text-lg font-bold">{record.name || ''}</h3>
|
||||||
|
|
||||||
|
<div class="join ml-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline join-item"
|
||||||
|
on:click={() => showDetails = !showDetails}
|
||||||
|
>
|
||||||
|
<Icon name="pencil" class="size-6" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline btn-error join-item"
|
||||||
|
on:click={conditionalDelete}
|
||||||
|
>
|
||||||
|
<Icon name="trash" class="size-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showDetails}
|
||||||
|
<form
|
||||||
|
transition:slide|local={{duration: 200}}
|
||||||
|
class=" px-6 pb-4 space-y-4"
|
||||||
|
on:submit|preventDefault={() => alert.run(saveCredential)}
|
||||||
|
>
|
||||||
|
<ErrorAlert bind:this={alert} />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
||||||
|
{#if record.isNew}
|
||||||
|
<span class="justify-self-end">File</span>
|
||||||
|
<FileInput bind:value={file} params={{defaultPath}} />
|
||||||
|
|
||||||
|
<span class="justify-self-end">Passphrase</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered bg-transparent"
|
||||||
|
bind:value={passphrase}
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<span class="justify-self-end">Algorithm</span>
|
||||||
|
<span class="font-mono">{record.credential.algorithm}</span>
|
||||||
|
|
||||||
|
<span class="justify-self-end">Comment</span>
|
||||||
|
<span class="font-mono">{record.credential.comment}</span>
|
||||||
|
|
||||||
|
<span class="justify-self-end">Public key</span>
|
||||||
|
<span class="font-mono">{record.credential.public_key}</span>
|
||||||
|
|
||||||
|
<span class="justify-self-end">Private key</span>
|
||||||
|
<span class="font-mono">{record.credential.private_key}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
{#if isModified}
|
||||||
|
<button
|
||||||
|
transition:fade={{duration: 100}}
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
Loading…
x
Reference in New Issue
Block a user