initial ssh key model and creation ui

This commit is contained in:
2024-07-01 06:38:46 -04:00
parent f311fde74e
commit 5e6542d08e
20 changed files with 555 additions and 28 deletions

39
src/ui/FileInput.svelte Normal file
View 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>

View File

@ -19,13 +19,13 @@
</style>
<div class="join w-full">
<div class="join w-full has-[:focus]:outline outline-2 outline-offset-2 outline-base-content/20">
<input
type={show ? 'text' : 'password'}
{value} {placeholder} {autofocus}
on:input={e => value = e.target.value}
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

View File

@ -5,12 +5,16 @@
import { invoke } from '@tauri-apps/api/core';
import AwsCredential from './credentials/AwsCredential.svelte';
import NewSshKey from './credentials/NewSshKey.svelte';
import Icon from '../ui/Icon.svelte';
import Nav from '../ui/Nav.svelte';
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({});
async function loadCreds() {
records = await invoke('list_credentials');
@ -24,7 +28,18 @@
id: crypto.randomUUID(),
name: null,
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,
});
records = records;
@ -36,27 +51,53 @@
<h1 slot="title" class="text-2xl font-bold">Credentials</h1>
</Nav>
<div class="max-w-xl mx-auto mb-12 flex flex-col gap-y-4 justify-center">
<div class="divider">
<h2 class="text-xl font-bold">AWS Access Keys</h2>
</div>
<div class="max-w-xl mx-auto mb-12 flex flex-col gap-y-12 justify-center">
<div class="flex flex-col gap-y-4">
<div class="divider">
<h2 class="text-xl font-bold">AWS Access Keys</h2>
</div>
{#if records.length > 0}
{#each records as record (record.id)}
<AwsCredential {record} {defaults} on:update={loadCreds} />
{/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>
{#if awsRecords.length > 0}
{#each awsRecords as record (record.id)}
<AwsCredential {record} {defaults} on:update={loadCreds} />
{/each}
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
<Icon name="plus-circle-mini" class="size-5" />
Add
</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>
{/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>

View File

@ -16,7 +16,6 @@
let showDetails = record.isNew ? true : false;
let localName = name;
let local = JSON.parse(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' : ''}"
>
<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}
<span class="badge badge-accent">Default</span>
@ -129,9 +134,9 @@
transition:fade={{duration: 100}}
type="submit"
class="btn btn-primary"
>
Save
</button>
>
Save
</button>
{/if}
</div>
</form>

View 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>

View 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>