almost finish refactoring PersistentCredential trait
This commit is contained in:
14
src/views/ChangePassphrase.svelte
Normal file
14
src/views/ChangePassphrase.svelte
Normal file
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import { navigate } from '../lib/routing.js';
|
||||
|
||||
import EnterPassphrase from './passphrase/EnterPassphrase.svelte';
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex flex-col h-screen max-w-sm m-auto gap-y-8 justify-center">
|
||||
<h1 class="text-2xl font-bold text-center">
|
||||
Change passphrase
|
||||
</h1>
|
||||
|
||||
<EnterPassphrase cancellable={true} on:save={() => navigate('Home')}/>
|
||||
</div>
|
21
src/views/CreatePassphrase.svelte
Normal file
21
src/views/CreatePassphrase.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script>
|
||||
import EnterPassphrase from './passphrase/EnterPassphrase.svelte';
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex flex-col h-screen max-w-lg m-auto justify-center">
|
||||
<div class="space-y-8">
|
||||
<h1 class="text-2xl font-bold text-center">Welcome to Creddy!</h1>
|
||||
|
||||
<div class="space-y-4">
|
||||
<p> Create a passphrase to get started.</p>
|
||||
|
||||
<p>Please note that if you forget your passphrase, there is no way to recover
|
||||
your stored credentials. You will have to start over with a new passphrase.</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-sm mx-auto">
|
||||
<EnterPassphrase />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
93
src/views/EnterAwsCredential.svelte
Normal file
93
src/views/EnterAwsCredential.svelte
Normal file
@ -0,0 +1,93 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { getRootCause } from '../lib/errors.js';
|
||||
|
||||
import { appState } from '../lib/state.js';
|
||||
import { navigate } from '../lib/routing.js';
|
||||
import Link from '../ui/Link.svelte';
|
||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||
import Spinner from '../ui/Spinner.svelte';
|
||||
|
||||
|
||||
let errorMsg = null;
|
||||
let alert;
|
||||
let AccessKeyId, SecretAccessKey, passphrase, confirmPassphrase
|
||||
|
||||
function confirm() {
|
||||
if (passphrase !== confirmPassphrase) {
|
||||
errorMsg = 'Passphrases do not match.'
|
||||
}
|
||||
}
|
||||
|
||||
let saving = false;
|
||||
async function save() {
|
||||
if (passphrase !== confirmPassphrase) {
|
||||
alert.shake();
|
||||
return;
|
||||
}
|
||||
|
||||
let credentials = {AccessKeyId, SecretAccessKey};
|
||||
try {
|
||||
saving = true;
|
||||
await invoke('save_credentials', {credentials, passphrase});
|
||||
emit('credentials-event', 'entered');
|
||||
if ($appState.currentRequest) {
|
||||
navigate('Approve');
|
||||
}
|
||||
else {
|
||||
navigate('Home');
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const root = getRootCause(e);
|
||||
if (e.code === 'GetSession' && root.code) {
|
||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
||||
}
|
||||
else {
|
||||
// some of the built-in Tauri errors are plain strings,
|
||||
// so fall back to e if e.msg doesn't exist
|
||||
errorMsg = e.msg || e;
|
||||
}
|
||||
|
||||
// if the alert already existed, shake it
|
||||
if (alert) {
|
||||
alert.shake();
|
||||
}
|
||||
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
emit('credentials-event', 'enter-canceled');
|
||||
navigate('Home');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<form action="#" on:submit|preventDefault="{save}" class="form-control space-y-4 max-w-sm m-auto p-4 h-screen justify-center">
|
||||
<h2 class="text-2xl font-bold text-center">Enter your credentials</h2>
|
||||
|
||||
{#if errorMsg}
|
||||
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
|
||||
{/if}
|
||||
|
||||
<input type="text" placeholder="AWS Access Key ID" bind:value="{AccessKeyId}" class="input input-bordered" />
|
||||
<input type="password" placeholder="AWS Secret Access Key" bind:value="{SecretAccessKey}" class="input input-bordered" />
|
||||
<input type="password" placeholder="Passphrase" bind:value="{passphrase}" class="input input-bordered" />
|
||||
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{#if saving }
|
||||
<Spinner class="w-5 h-5" thickness="12"/>
|
||||
{:else}
|
||||
Submit
|
||||
{/if}
|
||||
</button>
|
||||
<Link target={cancel} hotkey="Escape">
|
||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||
</Link>
|
||||
</form>
|
62
src/views/ManageCredentials.svelte
Normal file
62
src/views/ManageCredentials.svelte
Normal file
@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { slide, fade } from 'svelte/transition';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import AwsCredential from './credentials/AwsCredential.svelte';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
import Nav from '../ui/Nav.svelte';
|
||||
|
||||
let show = false;
|
||||
|
||||
let records = []
|
||||
async function loadCreds() {
|
||||
records = await invoke('list_credentials');
|
||||
console.log(records);
|
||||
}
|
||||
onMount(loadCreds);
|
||||
|
||||
function newCred() {
|
||||
console.log('hello!');
|
||||
records.push({
|
||||
id: crypto.randomUUID(),
|
||||
name: '',
|
||||
is_default: false,
|
||||
credential: {type: 'AwsBase', AccessKeyId: '', SecretAccessKey: ''},
|
||||
isNew: true,
|
||||
});
|
||||
records = records;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<Nav>
|
||||
<h1 slot="title" class="text-2xl font-bold">Credentials</h1>
|
||||
</Nav>
|
||||
|
||||
<div class="max-w-xl mx-auto flex flex-col gap-y-4 justify-center">
|
||||
<div class="divider">
|
||||
<h2 class="text-xl font-bold">AWS Access Keys</h2>
|
||||
</div>
|
||||
|
||||
{#if records.length > 0}
|
||||
<div class="rounded-box border-2 border-neutral-content/30 divide-y-2 divide-neutral-content/30">
|
||||
{#each records as record (record.id)}
|
||||
<AwsCredential {record} on:update={loadCreds} />
|
||||
{/each}
|
||||
</div>
|
||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newCred}>
|
||||
<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={newCred}>
|
||||
<Icon name="plus-circle-mini" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
91
src/views/approve/CollectResponse.svelte
Normal file
91
src/views/approve/CollectResponse.svelte
Normal file
@ -0,0 +1,91 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { appState, cleanupRequest } from '../../lib/state.js';
|
||||
|
||||
import Link from '../../ui/Link.svelte';
|
||||
import KeyCombo from '../../ui/KeyCombo.svelte';
|
||||
|
||||
|
||||
// Executable paths can be long, so ensure they only break on \ or /
|
||||
function breakPath(path) {
|
||||
return path.replace(/(\\|\/)/g, '$1<wbr>');
|
||||
}
|
||||
|
||||
// Extract executable name from full path
|
||||
const client = $appState.currentRequest.client;
|
||||
const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
||||
const appName = m[1] || m[2];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function setResponse(approval, base) {
|
||||
$appState.currentRequest.response = {
|
||||
id: $appState.currentRequest.id,
|
||||
approval,
|
||||
base,
|
||||
};
|
||||
dispatch('response');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if $appState.currentRequest?.base}
|
||||
<div class="alert alert-warning shadow-lg">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<span>
|
||||
WARNING: This application is requesting your base AWS credentials.
|
||||
These credentials are less secure than session credentials, since they don't expire automatically.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-1 mb-4">
|
||||
<h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
|
||||
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
||||
<div class="text-right">Path:</div>
|
||||
<code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code>
|
||||
<div class="text-right">PID:</div>
|
||||
<code>{client.pid}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full grid grid-cols-[1fr_auto] items-center gap-y-6">
|
||||
<!-- Don't display the option to approve with session credentials if base was specifically requested -->
|
||||
{#if !$appState.currentRequest?.base}
|
||||
<h3 class="font-semibold">
|
||||
Approve with session credentials
|
||||
</h3>
|
||||
<Link target={() => setResponse('Approved', false)} hotkey="Enter" shift={true}>
|
||||
<button class="w-full btn btn-success">
|
||||
<KeyCombo keys={['Shift', 'Enter']} />
|
||||
</button>
|
||||
</Link>
|
||||
{/if}
|
||||
|
||||
<h3 class="font-semibold">
|
||||
<span class="mr-2">
|
||||
{#if $appState.currentRequest?.base}
|
||||
Approve
|
||||
{:else}
|
||||
Approve with base credentials
|
||||
{/if}
|
||||
</span>
|
||||
</h3>
|
||||
<Link target={() => setResponse('Approved', true)} hotkey="Enter" shift={true} ctrl={true}>
|
||||
<button class="w-full btn btn-warning">
|
||||
<KeyCombo keys={['Ctrl', 'Shift', 'Enter']} />
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<h3 class="font-semibold">
|
||||
<span class="mr-2">Deny</span>
|
||||
</h3>
|
||||
<Link target={() => setResponse('Denied', false)} hotkey="Escape">
|
||||
<button class="w-full btn btn-error">
|
||||
<KeyCombo keys={['Esc']} />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
29
src/views/approve/ShowResponse.svelte
Normal file
29
src/views/approve/ShowResponse.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { draw, fade } from 'svelte/transition';
|
||||
|
||||
import { appState } from '../../lib/state.js';
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
let drawDuration = $appState.config.rehide_ms >= 750 ? 500 : 0;
|
||||
let fadeDuration = drawDuration * 0.6;
|
||||
let fadeDelay = drawDuration * 0.4;
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
|
||||
{#if $appState.currentRequest.response.approval === 'Approved'}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
|
||||
<path in:draw="{{duration: drawDuration}}" stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
|
||||
<path in:draw="{{duration: 500}}" stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
<div in:fade="{{duration: fadeDuration, delay: fadeDelay}}" class="text-2xl font-bold">
|
||||
{$appState.currentRequest.response.approval}!
|
||||
</div>
|
||||
</div>
|
157
src/views/credentials/AwsCredential.svelte
Normal file
157
src/views/credentials/AwsCredential.svelte
Normal file
@ -0,0 +1,157 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||
import Icon from '../../ui/Icon.svelte';
|
||||
|
||||
export let record
|
||||
|
||||
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// if record.credential is blank when component is first instantiated, this is
|
||||
// a newly-added credential, so show details so that data can be filled out
|
||||
let showDetails = record.isNew ? true : false;
|
||||
|
||||
let localName = name;
|
||||
let local = JSON.parse(JSON.stringify(record));
|
||||
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
||||
|
||||
let error, alert;
|
||||
async function saveCredential() {
|
||||
try {
|
||||
await invoke('save_credential', {cred: local});
|
||||
dispatch('update');
|
||||
showDetails = false;
|
||||
}
|
||||
catch (e) {
|
||||
if (error) alert.shake();
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let confirmDelete;
|
||||
function conditionalDelete() {
|
||||
if (!record.isNew) {
|
||||
confirmDelete.showModal();
|
||||
}
|
||||
else {
|
||||
deleteCredential();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCredential() {
|
||||
try {
|
||||
if (!record.isNew) {
|
||||
|
||||
await invoke('delete_credential', {id: record.id});
|
||||
}
|
||||
dispatch('update');
|
||||
}
|
||||
catch (e) {
|
||||
if (error) alert.shake();
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div transition:slide|local={{duration: record.isNew ? 300 : 0}} class="px-6 py-4 space-y-4">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<h3 class="text-lg font-bold">{record.name}</h3>
|
||||
|
||||
{#if record.is_default}
|
||||
<span class="badge badge-secondary">Default</span>
|
||||
{/if}
|
||||
|
||||
<div class="join ml-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary join-item"
|
||||
on:click={() => showDetails = !showDetails}
|
||||
>
|
||||
<Icon name="pencil" class="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error join-item"
|
||||
on:click={conditionalDelete}
|
||||
>
|
||||
<Icon name="trash" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#if showDetails}
|
||||
{#if error}
|
||||
<ErrorAlert bind:this={alert}>{error}</ErrorAlert>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
transition:slide|local={{duration: 200}}
|
||||
class="space-y-4"
|
||||
on:submit|preventDefault={saveCredential}
|
||||
>
|
||||
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
||||
{#if record.isNew}
|
||||
<span class="justify-self-end">Name</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={local.name}
|
||||
>
|
||||
{/if}
|
||||
|
||||
<span class="justify-self-end">Key ID</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered font-mono"
|
||||
bind:value={local.credential.AccessKeyId}
|
||||
>
|
||||
|
||||
<span>Secret key</span>
|
||||
<div class="font-mono">
|
||||
<PassphraseInput bind:value={local.credential.SecretAccessKey} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<label class="label cursor-pointer justify-self-start space-x-4">
|
||||
<span class="label-text">Default for type</span>
|
||||
<input type="checkbox" class="toggle toggle-secondary" bind:checked={local.is_default}>
|
||||
</label>
|
||||
{#if isModified}
|
||||
<button
|
||||
transition:fade={{duration: 100}}
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<dialog bind:this={confirmDelete} class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Delete AWS credential "{record.name}"?</h3>
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="flex gap-x-4">
|
||||
<button class="btn btn-outline">Cancel</button>
|
||||
<button
|
||||
autofocus
|
||||
class="btn btn-error"
|
||||
on:click={deleteCredential}
|
||||
>Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
84
src/views/passphrase/EnterPassphrase.svelte
Normal file
84
src/views/passphrase/EnterPassphrase.svelte
Normal file
@ -0,0 +1,84 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { appState } from '../../lib/state.js';
|
||||
|
||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||
import Link from '../../ui/Link.svelte';
|
||||
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||
import Spinner from '../../ui/Spinner.svelte';
|
||||
|
||||
export let cancellable = false;
|
||||
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let alert, saving;
|
||||
let passphrase = '';
|
||||
let confirmPassphrase = '';
|
||||
let error = null;
|
||||
|
||||
function confirm() {
|
||||
if (passphrase !== confirmPassphrase) {
|
||||
error = 'Passphrases do not match.';
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (passphrase === '' || passphrase !== confirmPassphrase) {
|
||||
alert.shake();
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
try {
|
||||
await invoke('set_passphrase', {passphrase});
|
||||
$appState.sessionStatus = 'unlocked';
|
||||
dispatch('save');
|
||||
}
|
||||
catch (e) {
|
||||
if (error) alert.shake();
|
||||
error = e;
|
||||
}
|
||||
saving = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<form class="form-control gap-y-4" on:submit|preventDefault={save}>
|
||||
{#if error}
|
||||
<ErrorAlert bind:this={alert}>{error}</ErrorAlert>
|
||||
{/if}
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Passphrase</span>
|
||||
</div>
|
||||
<PassphraseInput bind:value={passphrase} placeholder="correct horse battery staple" />
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Re-enter passphrase</span>
|
||||
</div>
|
||||
<PassphraseInput
|
||||
bind:value={confirmPassphrase}
|
||||
placeholder="correct horse battery staple"
|
||||
on:change={confirm}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{#if saving}
|
||||
<Spinner class="w-5 h-5" thickness="12"/>
|
||||
{:else}
|
||||
Submit
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if cancellable}
|
||||
<Link target="Home" hotkey="Escape">
|
||||
<button type="button" class="btn btn-outline btn-sm w-full">Cancel</button>
|
||||
</Link>
|
||||
{/if}
|
||||
</form>
|
Reference in New Issue
Block a user