settings page

This commit is contained in:
Joseph Montanaro 2023-04-25 22:10:14 -07:00
parent 6f9cd6b471
commit 35271049dd
16 changed files with 210 additions and 90 deletions

View File

@ -3,7 +3,8 @@ CREATE TABLE credentials (
access_key_id TEXT NOT NULL, access_key_id TEXT NOT NULL,
secret_key_enc BLOB NOT NULL, secret_key_enc BLOB NOT NULL,
salt BLOB NOT NULL, salt BLOB NOT NULL,
nonce BLOB NOT NULL nonce BLOB NOT NULL,
created_at INTEGER NOT NULL
); );
CREATE TABLE config ( CREATE TABLE config (

View File

@ -67,3 +67,10 @@ pub fn get_config(app_state: State<'_, AppState>) -> AppConfig {
let config = app_state.config.read().unwrap(); let config = app_state.config.read().unwrap();
config.clone() config.clone()
} }
#[tauri::command]
pub fn save_config(config: AppConfig, app_state: State<'_, AppState>) {
let mut prev_config = app_state.config.write().unwrap();
*prev_config = config;
}

View File

@ -36,6 +36,7 @@ fn main() {
ipc::get_session_status, ipc::get_session_status,
ipc::save_credentials, ipc::save_credentials,
ipc::get_config, ipc::get_config,
ipc::save_config,
]) ])
.setup(|app| { .setup(|app| {
APP.set(app.handle()).unwrap(); APP.set(app.handle()).unwrap();

View File

@ -91,7 +91,7 @@ impl AppState {
} }
async fn load_creds(pool: &SqlitePool) -> Result<Session, SetupError> { async fn load_creds(pool: &SqlitePool) -> Result<Session, SetupError> {
let res = sqlx::query!("SELECT * FROM credentials") let res = sqlx::query!("SELECT * FROM credentials ORDER BY created_at desc")
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
let row = match res { let row = match res {
@ -122,6 +122,10 @@ impl AppState {
}, },
_ => unreachable!(), _ => unreachable!(),
}; };
// do this first so that if it fails we don't save bad credentials
self.new_session(&key_id, &secret_key).await?;
let salt = pwhash::gen_salt(); let salt = pwhash::gen_salt();
let mut key_buf = [0; secretbox::KEYBYTES]; let mut key_buf = [0; secretbox::KEYBYTES];
pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &salt).unwrap(); pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &salt).unwrap();
@ -133,8 +137,8 @@ impl AppState {
sqlx::query( sqlx::query(
"INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce) "INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at)
VALUES (?, ?, ?, ?)" VALUES (?, ?, ?, ?, strftime('%s'))"
) )
.bind(&key_id) .bind(&key_id)
.bind(&secret_key_enc) .bind(&secret_key_enc)
@ -143,8 +147,6 @@ impl AppState {
.execute(&self.pool) .execute(&self.pool)
.await?; .await?;
self.new_session(&key_id, &secret_key).await?;
Ok(()) Ok(())
} }

View File

@ -3,16 +3,20 @@ import { emit, listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
import { appState } from './lib/state.js'; import { appState } from './lib/state.js';
import { currentView } from './lib/routing.js'; import { navigate, currentView } from './lib/routing.js';
invoke('get_config').then(config => $appState.config = config);
listen('credentials-request', (tauriEvent) => { listen('credentials-request', (tauriEvent) => {
$appState.pendingRequests.put(tauriEvent.payload); $appState.pendingRequests.put(tauriEvent.payload);
}); });
// can't set this in routing.js directly for some reason
if (!$currentView) {
navigate('Home');
}
</script> </script>
<svelte:component <svelte:component this="{$currentView}" />
this="{$currentView}"
/>
<!-- <svelte:component this="{VIEWS['./views/ShowApproved.svelte'].default}" bind:appState="{appState}" /> -->

View File

@ -1,8 +1,7 @@
import { writable, derived } from 'svelte/store'; import { writable } from 'svelte/store';
const VIEWS = import.meta.glob('../views/*.svelte', {eager: true}); const VIEWS = import.meta.glob('../views/*.svelte', {eager: true});
export let currentView = writable(); export let currentView = writable();
export function navigate(viewName) { export function navigate(viewName) {
@ -10,4 +9,6 @@ export function navigate(viewName) {
currentView.set(view); currentView.set(view);
} }
navigate('Home'); export function getView(viewName) {
return VIEWS[`../views/${viewName}.svelte`].default;
}

View File

@ -2,6 +2,8 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
let extraClasses;
export {extraClasses as class};
export let slideDuration = 150; export let slideDuration = 150;
let animationClass = ""; let animationClass = "";
@ -49,7 +51,7 @@
</style> </style>
<div in:slide="{{duration: slideDuration}}" class="alert alert-error shadow-lg {animationClass}"> <div in:slide="{{duration: slideDuration}}" class="alert alert-error shadow-lg {animationClass} {extraClasses}">
<div> <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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> <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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span> <span>

View File

@ -1,7 +1,5 @@
<script> <script>
import { onMount } from 'svelte'; import { navigate, currentView } from '../lib/routing.js';
import { navigate } from '../lib/routing.js';
export let target; export let target;
export let hotkey = null; export let hotkey = null;
@ -28,8 +26,12 @@
if (alt && !event.altKey) return; if (alt && !event.altKey) return;
if (shift && !event.shiftKey) return; if (shift && !event.shiftKey) return;
if (event.code === hotkey) click(); if (event.code === hotkey) {
if (hotkey === 'Enter' && event.code === 'NumpadEnter') click(); click();
}
else if (hotkey === 'Enter' && event.code === 'NumpadEnter') {
click();
}
} }
</script> </script>

View File

@ -0,0 +1,62 @@
<script>
import { createEventDispatcher } from 'svelte';
import Setting from './Setting.svelte';
export let title;
export let value;
export let unit = '';
export let min = null;
export let max = null;
export let decimal = false;
const dispatch = createEventDispatcher();
let error = null;
function validate(event) {
let v = event.target.value;
if (v === '') {
error = null;
return;
}
let num = parseFloat(v);
if (Number.isNaN(num)) {
error = `"${v}" is not a number`;
}
else if (num % 1 !== 0 && !decimal) {
error = `${num} is not a whole number`;
}
else if (min && num < min) {
error = `Too low (minimum ${min})`;
}
else if (max && num > max) {
error = `Too large (maximum ${max})`
}
else {
error = null;
value = num;
dispatch('update', {value})
}
}
</script>
<Setting {title} {error}>
<div slot="input">
{#if unit}
<span class="mr-2">{unit}:</span>
{/if}
<div class="tooltip tooltip-error" class:tooltip-open={error !== null} data-tip={error}>
<input
type="text"
class="input input-sm input-bordered text-right max-w-[4rem]"
class:input-error={error}
value={value}
on:input="{validate}"
/>
</div>
</div>
<slot name="description" slot="description"></slot>
</Setting>

View File

@ -0,0 +1,18 @@
<script>
import { slide } from 'svelte/transition';
import ErrorAlert from '../ErrorAlert.svelte';
export let title;
export let error = null;
</script>
<div class="divider"></div>
<div class="flex justify-between">
<h3 class="text-lg font-bold">{title}</h3>
<slot name="input"></slot>
</div>
<p class="mt-3">
<slot name="description"></slot>
</p>

View File

@ -0,0 +1,22 @@
<script>
import { createEventDispatcher } from 'svelte';
import Setting from './Setting.svelte';
export let title;
export let value;
const dispatch = createEventDispatcher();
</script>
<Setting title="Start minimized">
<input
slot="input"
type="checkbox"
class="toggle toggle-success"
bind:checked={value}
on:change={e => dispatch('update', {value: e.target.checked})}
/>
<slot name="description" slot="description"></slot>
</Setting>

3
src/ui/settings/index.js Normal file
View File

@ -0,0 +1,3 @@
export { default as Setting } from './Setting.svelte';
export { default as ToggleSetting } from './ToggleSetting.svelte';
export { default as NumericSetting } from './NumericSetting.svelte';

View File

@ -15,33 +15,28 @@
$appState.currentRequest = req; $appState.currentRequest = req;
navigate('Approve'); navigate('Approve');
}); });
let status = 'unknown';
onMount(async() => {
status = await invoke('get_session_status');
})
</script> </script>
<Nav /> <Nav />
{#if status === 'locked'} <div class="flex flex-col h-screen justify-center items-center space-y-4">
<div class="flex flex-col h-screen justify-center items-center space-y-4"> {#await invoke('get_session_status') then status}
<img src="/static/padlock-closed.svg" alt="An unlocked padlock" class="w-32" /> {#if status === 'locked'}
<img src="/static/padlock-closed.svg" alt="A locked padlock" class="w-32" />
<h2 class="text-2xl font-bold">Creddy is locked</h2> <h2 class="text-2xl font-bold">Creddy is locked</h2>
<Link target="Unlock"> <Link target="Unlock">
<button class="btn btn-primary">Unlock</button> <button class="btn btn-primary">Unlock</button>
</Link> </Link>
</div>
{:else if status === 'unlocked'} {:else if status === 'unlocked'}
<div class="flex flex-col h-screen justify-center items-center space-y-4">
<img src="/static/padlock-open.svg" alt="An unlocked padlock" class="w-24" /> <img src="/static/padlock-open.svg" alt="An unlocked padlock" class="w-24" />
<h2 class="text-2xl font-bold">Waiting for requests</h2> <h2 class="text-2xl font-bold">Waiting for requests</h2>
</div>
{:else if status === 'empty'} {:else if status === 'empty'}
<Link target="EnterCredentials"> <Link target="EnterCredentials">
<button class="btn btn-primary">Enter Credentials</button> <button class="btn btn-primary">Enter Credentials</button>
</Link> </Link>
{/if} {/if}
{/await}
</div>

View File

@ -1,44 +1,46 @@
<script> <script>
import { invoke } from '@tauri-apps/api/tauri';
import { appState } from '../lib/state.js';
import Nav from '../ui/Nav.svelte'; import Nav from '../ui/Nav.svelte';
import Link from '../ui/Link.svelte'; import Link from '../ui/Link.svelte';
import ErrorAlert from '../ui/ErrorAlert.svelte';
// import Setting from '../ui/settings/Setting.svelte';
import { Setting, ToggleSetting, NumericSetting } from '../ui/settings';
async function save() {
await invoke('save_config', {config: $appState.config});
}
</script> </script>
<Nav /> <Nav />
<div class="mx-auto mt-3 max-w-md"> {#await invoke('get_config') then config}
<div class="mx-auto mt-3 max-w-md">
<h2 class="text-2xl font-bold text-center">Settings</h2> <h2 class="text-2xl font-bold text-center">Settings</h2>
<div class="divider"></div> <ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
<div class="grid grid-cols-2 items-center"> <svelte:fragment slot="description">
<h3 class="text-lg font-bold">Start minimized</h3> Minimize to the system tray at startup.
<input type="checkbox" class="justify-self-end toggle toggle-success" /> </svelte:fragment>
</div> </ToggleSetting>
<p class="mt-3">Minimize to the system tray at startup.</p>
<div class="divider"></div> <NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
<div class="grid grid-cols-2 items-center"> <svelte:fragment slot="description">
<h3 class="text-lg font-bold">Re-hide delay</h3>
<div class="justify-self-end">
<span class="mr-2">(Seconds)</span>
<input type="text" class="input input-sm input-bordered text-right max-w-[4rem]" />
</div>
</div>
<p class="mt-3">
How long to wait after a request is approved/denied before minimizing How long to wait after a request is approved/denied before minimizing
the window to tray. Only applicable if the window was minimized the window to tray. Only applicable if the window was minimized
to tray before the request was received. to tray before the request was received.
</p> </svelte:fragment>
</NumericSetting>
<div class="divider"></div> <Setting title="Update credentials">
<div class="grid grid-cols-2 items-center"> <Link slot="input" target="EnterCredentials">
<h3 class="text-lg font-bold">Update credentials</h3>
<div class="justify-self-end">
<Link target="EnterCredentials">
<button class="btn btn-sm btn-primary">Update</button> <button class="btn btn-sm btn-primary">Update</button>
</Link> </Link>
<svelte:fragment slot="description">
Update or re-enter your encrypted credentials.
</svelte:fragment>
</Setting>
</div> </div>
</div> {/await}
<p class="mt-3">Update or re-enter your encrypted credentials.</p>
</div>

View File

@ -44,14 +44,13 @@
<div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg"> <div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg">
<ErrorAlert> <ErrorAlert>
{error} {error}
<svelte:fragment slot="buttons">
<Link target="Home"> <Link target="Home">
<button <button class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content">
slot="buttons"
class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content"
>
Ok Ok
</button> </button>
</Link> </Link>
</svelte:fragment>
</ErrorAlert> </ErrorAlert>
</div> </div>
{:else if success} {:else if success}

View File

@ -36,14 +36,13 @@
<div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg"> <div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg">
<ErrorAlert> <ErrorAlert>
{error} {error}
<svelte:fragment slot="buttons">
<Link target="Home"> <Link target="Home">
<button <button class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content" on:click="{() => navigate('Home')}">
slot="buttons"
class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content"
>
Ok Ok
</button> </button>
</Link> </Link>
</svelte:fragment>
</ErrorAlert> </ErrorAlert>
</div> </div>
{:else} {:else}