request unlock/credentials when terminal is launched from locked/empty state
This commit is contained in:
parent
8d7b01629d
commit
61d9acc7c6
@ -25,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.2", features = ["dialog", "dialog-open", "os-all", "system-tray", "global-shortcut"] }
|
tauri = { version = "1.2", features = ["dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] }
|
||||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||||
sodiumoxide = "0.2.7"
|
sodiumoxide = "0.2.7"
|
||||||
tokio = { version = ">=1.19", features = ["full"] }
|
tokio = { version = ">=1.19", features = ["full"] }
|
||||||
|
@ -10,7 +10,6 @@ use tauri::{
|
|||||||
App,
|
App,
|
||||||
AppHandle,
|
AppHandle,
|
||||||
Manager,
|
Manager,
|
||||||
GlobalShortcutManager,
|
|
||||||
async_runtime as rt,
|
async_runtime as rt,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,17 +26,17 @@ pub struct TermConfig {
|
|||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
pub struct HotkeysConfig {
|
pub struct Hotkey {
|
||||||
// tauri uses strings to represent keybinds, so we will as well
|
pub keys: String,
|
||||||
pub show_window: Hotkey,
|
pub enabled: bool,
|
||||||
pub launch_terminal: Hotkey,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
pub struct Hotkey {
|
pub struct HotkeysConfig {
|
||||||
pub keys: String,
|
// tauri uses strings to represent keybinds, so we will as well
|
||||||
pub enabled: bool,
|
pub show_window: Hotkey,
|
||||||
|
pub launch_terminal: Hotkey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -244,6 +244,19 @@ pub enum ExecError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum LaunchTerminalError {
|
||||||
|
#[error("Could not discover main window")]
|
||||||
|
NoMainWindow,
|
||||||
|
#[error("Failed to communicate with main Creddy window")]
|
||||||
|
IpcFailed(#[from] tauri::Error),
|
||||||
|
#[error("Failed to launch terminal: {0}")]
|
||||||
|
Exec(#[from] ExecError),
|
||||||
|
#[error(transparent)]
|
||||||
|
GetCredentials(#[from] GetCredentialsError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Serialize implementations
|
// Serialize implementations
|
||||||
// =========================
|
// =========================
|
||||||
@ -349,3 +362,18 @@ impl Serialize for ExecError {
|
|||||||
map.end()
|
map.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Serialize for LaunchTerminalError {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
map.serialize_entry("code", self.as_ref())?;
|
||||||
|
map.serialize_entry("msg", &format!("{self}"))?;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
LaunchTerminalError::Exec(src) => map.serialize_entry("source", &src)?,
|
||||||
|
_ => serialize_upstream_err(self, &mut map)?,
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -82,6 +82,6 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R
|
|||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn launch_terminal(base: bool) -> Result<(), ExecError> {
|
pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
terminal::launch(base).await
|
terminal::launch(base).await
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ pub struct AppState {
|
|||||||
pub session: RwLock<Session>,
|
pub session: RwLock<Session>,
|
||||||
pub request_count: RwLock<u64>,
|
pub request_count: RwLock<u64>,
|
||||||
pub open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
|
pub open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
|
||||||
|
pub pending_terminal_request: RwLock<bool>,
|
||||||
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
|
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
|
||||||
server: RwLock<Server>,
|
server: RwLock<Server>,
|
||||||
pool: sqlx::SqlitePool,
|
pool: sqlx::SqlitePool,
|
||||||
@ -41,6 +42,7 @@ impl AppState {
|
|||||||
session: RwLock::new(session),
|
session: RwLock::new(session),
|
||||||
request_count: RwLock::new(0),
|
request_count: RwLock::new(0),
|
||||||
open_requests: RwLock::new(HashMap::new()),
|
open_requests: RwLock::new(HashMap::new()),
|
||||||
|
pending_terminal_request: RwLock::new(false),
|
||||||
bans: RwLock::new(HashSet::new()),
|
bans: RwLock::new(HashSet::new()),
|
||||||
server: RwLock::new(server),
|
server: RwLock::new(server),
|
||||||
pool,
|
pool,
|
||||||
@ -149,6 +151,11 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_unlocked(&self) -> bool {
|
||||||
|
let session = self.session.read().await;
|
||||||
|
matches!(*session, Session::Unlocked{..})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
|
pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
|
||||||
let app_session = self.session.read().await;
|
let app_session = self.session.read().await;
|
||||||
let (base, _session) = app_session.try_get()?;
|
let (base, _session) = app_session.try_get()?;
|
||||||
@ -167,4 +174,21 @@ impl AppState {
|
|||||||
*app_session = Session::Unlocked {base, session};
|
*app_session = Session::Unlocked {base, session};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn register_terminal_request(&self) -> Result<(), ()> {
|
||||||
|
let mut req = self.pending_terminal_request.write().await;
|
||||||
|
if *req {
|
||||||
|
// if a request is already pending, we can't register a new one
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
*req = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unregister_terminal_request(&self) {
|
||||||
|
let mut req = self.pending_terminal_request.write().await;
|
||||||
|
*req = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,15 @@ use crate::errors::*;
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
|
||||||
pub async fn launch(use_base: bool) -> Result<(), ExecError> {
|
pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
let state = APP.get().unwrap().state::<AppState>();
|
let app = APP.get().unwrap();
|
||||||
// do all this in a block so we don't hold the lock any longer than necessary
|
let state = app.state::<AppState>();
|
||||||
|
|
||||||
|
// register_terminal_request() returns Err if there is another request pending
|
||||||
|
if state.register_terminal_request().await.is_err() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let mut cmd = {
|
let mut cmd = {
|
||||||
let config = state.config.read().await;
|
let config = state.config.read().await;
|
||||||
let mut cmd = Command::new(&config.terminal.exec);
|
let mut cmd = Command::new(&config.terminal.exec);
|
||||||
@ -17,10 +23,38 @@ pub async fn launch(use_base: bool) -> Result<(), ExecError> {
|
|||||||
cmd
|
cmd
|
||||||
};
|
};
|
||||||
|
|
||||||
// similarly
|
// if session is unlocked or empty, wait for credentials from frontend
|
||||||
|
if !state.is_unlocked().await {
|
||||||
|
app.emit_all("launch-terminal-request", ())?;
|
||||||
|
let window = app.get_window("main")
|
||||||
|
.ok_or(LaunchTerminalError::NoMainWindow)?;
|
||||||
|
if !window.is_visible()? {
|
||||||
|
window.unminimize()?;
|
||||||
|
window.show()?;
|
||||||
|
}
|
||||||
|
window.set_focus()?;
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
app.once_global("credentials-event", move |e| {
|
||||||
|
let success = match e.payload() {
|
||||||
|
Some("\"unlocked\"") | Some("\"entered\"") => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let _ = tx.send(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
if !rx.await.unwrap_or(false) {
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
return Ok(()); // request was canceled by user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// more lock-management
|
||||||
{
|
{
|
||||||
let state = APP.get().unwrap().state::<AppState>();
|
|
||||||
let app_session = state.session.read().await;
|
let app_session = state.session.read().await;
|
||||||
|
// session should really be unlocked at this point, but if the frontend misbehaves
|
||||||
|
// (i.e. lies about unlocking) we could end up here with a locked session
|
||||||
|
// this will result in an error popup to the user (see main hotkey handler)
|
||||||
let (base_creds, session_creds) = app_session.try_get()?;
|
let (base_creds, session_creds) = app_session.try_get()?;
|
||||||
if use_base {
|
if use_base {
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
||||||
@ -33,11 +67,16 @@ pub async fn launch(use_base: bool) -> Result<(), ExecError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match cmd.spawn() {
|
let res = match cmd.spawn() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) if std::io::ErrorKind::NotFound == e.kind() => {
|
Err(e) if std::io::ErrorKind::NotFound == e.kind() => {
|
||||||
Err(ExecError::NotFound(cmd.get_program().to_owned()))
|
Err(ExecError::NotFound(cmd.get_program().to_owned()))
|
||||||
},
|
},
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(ExecError::ExecutionFailed(e)),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
|
||||||
|
res?; // ? auto-conversion is more liberal than .into()
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,20 @@ listen('credentials-request', (tauriEvent) => {
|
|||||||
$appState.pendingRequests.put(tauriEvent.payload);
|
$appState.pendingRequests.put(tauriEvent.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
listen('launch-terminal-request', async (tauriEvent) => {
|
||||||
|
if ($appState.currentRequest === null) {
|
||||||
|
let status = await invoke('get_session_status');
|
||||||
|
if (status === 'locked') {
|
||||||
|
navigate('Unlock');
|
||||||
|
}
|
||||||
|
else if (status === 'empty') {
|
||||||
|
navigate('EnterCredentials');
|
||||||
|
}
|
||||||
|
// else, session is unlocked, so do nothing
|
||||||
|
// (although we shouldn't even get the event in that case)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
acceptRequest();
|
acceptRequest();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@ export default function() {
|
|||||||
|
|
||||||
resolvers: [],
|
resolvers: [],
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.items.length;
|
||||||
|
},
|
||||||
|
|
||||||
put(item) {
|
put(item) {
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
let resolver = this.resolvers.shift();
|
let resolver = this.resolvers.shift();
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
<div class="flex gap-x-[0.2em] items-center">
|
<div class="flex gap-x-[0.2em] items-center">
|
||||||
{#each keys as key, i}
|
{#each keys as key, i}
|
||||||
{#if i > 0} + {/if}
|
{#if i > 0}
|
||||||
|
<span class="mt-[-0.1em]">+</span>
|
||||||
|
{/if}
|
||||||
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">{key}</kbd>
|
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">{key}</kbd>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
try {
|
try {
|
||||||
saving = true;
|
saving = true;
|
||||||
await invoke('save_credentials', {credentials, passphrase});
|
await invoke('save_credentials', {credentials, passphrase});
|
||||||
|
emit('credentials-event', 'entered');
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('Approve');
|
||||||
}
|
}
|
||||||
@ -56,6 +57,11 @@
|
|||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('credentials-event', 'enter-canceled');
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -79,7 +85,7 @@
|
|||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target={cancel} hotkey="Escape">
|
||||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
@ -26,6 +27,7 @@
|
|||||||
saving = true;
|
saving = true;
|
||||||
let r = await invoke('unlock', {passphrase});
|
let r = await invoke('unlock', {passphrase});
|
||||||
$appState.credentialStatus = 'unlocked';
|
$appState.credentialStatus = 'unlocked';
|
||||||
|
emit('credentials-event', 'unlocked');
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('Approve');
|
||||||
}
|
}
|
||||||
@ -51,6 +53,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('credentials-event', 'unlock-canceled');
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadTime = Date.now();
|
loadTime = Date.now();
|
||||||
})
|
})
|
||||||
@ -75,7 +82,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target={cancel} hotkey="Escape">
|
||||||
<button class="btn btn-outline btn-sm w-full">Cancel</button>
|
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user