Compare commits
7 Commits
13545ac725
...
v0.4.6
Author | SHA1 | Date | |
---|---|---|---|
141334f7e2 | |||
69f6a39396 | |||
70e23c7e20 | |||
1df849442e | |||
7fdb336c79 | |||
46b8d810c5 | |||
dd40eb379e |
@ -1,13 +1,17 @@
|
||||
## Definitely
|
||||
|
||||
* Switch to "process" provider for AWS credentials (much less hacky)
|
||||
* ~~Switch to "process" provider for AWS credentials (much less hacky)~~
|
||||
* ~~Frontend needs to react when request is cancelled from backend~~
|
||||
* Session timeout (plain duration, or activity-based?)
|
||||
* ~Fix rehide behavior when new request comes in while old one is still being resolved~
|
||||
* Indicate on approval screen when additional requests are pending
|
||||
* ~~Fix rehide behavior when new request comes in while old one is still being resolved~~
|
||||
* Additional hotkey configuration (approve/deny at the very least)
|
||||
* ~~Switch tray menu item to Hide when window is visible~~
|
||||
* Logging
|
||||
* Icon
|
||||
* Auto-updates
|
||||
* SSH key handling
|
||||
* Encrypted sync server
|
||||
|
||||
## Maybe
|
||||
|
||||
|
1854
package-lock.json
generated
1854
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "creddy",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.5",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
27
src-tauri/Cargo.lock
generated
27
src-tauri/Cargo.lock
generated
@ -1035,7 +1035,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "creddy"
|
||||
version = "0.4.0"
|
||||
version = "0.4.6"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"auto-launch",
|
||||
@ -1059,6 +1059,7 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-single-instance",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"which",
|
||||
"windows 0.51.1",
|
||||
@ -1202,10 +1203,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.8"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -3158,6 +3160,12 @@ dependencies = [
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@ -4536,12 +4544,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.28"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
|
||||
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa 1.0.9",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
@ -4549,15 +4558,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.14"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
|
||||
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "creddy"
|
||||
version = "0.4.1"
|
||||
version = "0.4.6"
|
||||
description = "A friendly AWS credentials manager"
|
||||
authors = ["Joseph Montanaro"]
|
||||
license = ""
|
||||
@ -25,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] }
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2", features = ["dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] }
|
||||
tauri = { version = "1.2", features = [ "app-all", "dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] }
|
||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||
sodiumoxide = "0.2.7"
|
||||
tokio = { version = ">=1.19", features = ["full"] }
|
||||
@ -47,6 +47,7 @@ argon2 = { version = "0.5.0", features = ["std"] }
|
||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||
which = "4.4.0"
|
||||
windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] }
|
||||
time = "0.3.31"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::error::Error;
|
||||
use std::time::Duration;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use sqlx::{
|
||||
@ -31,8 +32,8 @@ pub static APP: OnceCell<AppHandle> = OnceCell::new();
|
||||
pub fn run() -> tauri::Result<()> {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
|
||||
app.get_window("main")
|
||||
.map(|w| w.show().error_popup("Failed to show main window"));
|
||||
show_main_window(app)
|
||||
.error_popup("Failed to show main window");
|
||||
}))
|
||||
.system_tray(tray::create())
|
||||
.on_system_tray_event(tray::handle_event)
|
||||
@ -40,6 +41,7 @@ pub fn run() -> tauri::Result<()> {
|
||||
ipc::unlock,
|
||||
ipc::respond,
|
||||
ipc::get_session_status,
|
||||
ipc::signal_activity,
|
||||
ipc::save_credentials,
|
||||
ipc::get_config,
|
||||
ipc::save_config,
|
||||
@ -49,9 +51,9 @@ pub fn run() -> tauri::Result<()> {
|
||||
.setup(|app| rt::block_on(setup(app)))
|
||||
.build(tauri::generate_context!())?
|
||||
.run(|app, run_event| match run_event {
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => match event {
|
||||
tauri::RunEvent::WindowEvent { event, .. } => match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
let _ = app.get_window(&label).map(|w| w.hide());
|
||||
let _ = hide_main_window(app);
|
||||
api.prevent_close();
|
||||
}
|
||||
_ => ()
|
||||
@ -114,12 +116,61 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
// if session is empty, this is probably the first launch, so don't autohide
|
||||
if !conf.start_minimized || is_first_launch {
|
||||
app.get_window("main")
|
||||
.ok_or(HandlerError::NoMainWindow)?
|
||||
.show()?;
|
||||
show_main_window(&app.handle())?;
|
||||
}
|
||||
|
||||
let state = AppState::new(conf, session, pool, setup_errors, desktop_is_gnome);
|
||||
app.manage(state);
|
||||
|
||||
// make sure we do this after managing app state, so that it doesn't panic
|
||||
start_auto_locker(app.app_handle());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn start_auto_locker(app: AppHandle) {
|
||||
rt::spawn(async move {
|
||||
let state = app.state::<AppState>();
|
||||
loop {
|
||||
// this gives our session-timeout a minimum resolution of 10s, which seems fine?
|
||||
let delay = Duration::from_secs(10);
|
||||
tokio::time::sleep(delay).await;
|
||||
|
||||
if state.should_auto_lock().await {
|
||||
state.lock().await.error_popup("Failed to lock Creddy");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> {
|
||||
let w = app.get_window("main").ok_or(WindowError::NoMainWindow)?;
|
||||
w.show()?;
|
||||
app.tray_handle()
|
||||
.get_item("show_hide")
|
||||
.set_title("Hide")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> {
|
||||
let w = app.get_window("main").ok_or(WindowError::NoMainWindow)?;
|
||||
w.hide()?;
|
||||
app.tray_handle()
|
||||
.get_item("show_hide")
|
||||
.set_title("Show")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn toggle_main_window(app: &AppHandle) -> Result<(), WindowError> {
|
||||
let w = app.get_window("main").ok_or(WindowError::NoMainWindow)?;
|
||||
if w.is_visible()? {
|
||||
hide_main_window(app)
|
||||
}
|
||||
else {
|
||||
show_main_window(app)
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ pub struct Client {
|
||||
|
||||
|
||||
pub fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
||||
dbg!(pid);
|
||||
let sys_pid = Pid::from_u32(pid);
|
||||
let mut sys = System::new();
|
||||
sys.refresh_process(sys_pid);
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use auto_launch::AutoLaunchBuilder;
|
||||
use is_terminal::IsTerminal;
|
||||
@ -45,6 +46,10 @@ impl HotkeysConfig {
|
||||
pub struct AppConfig {
|
||||
#[serde(default = "default_rehide_ms")]
|
||||
pub rehide_ms: u64,
|
||||
#[serde(default = "default_auto_lock")]
|
||||
pub auto_lock: bool,
|
||||
#[serde(default = "default_lock_after")]
|
||||
pub lock_after: Duration,
|
||||
#[serde(default = "default_start_minimized")]
|
||||
pub start_minimized: bool,
|
||||
#[serde(default = "default_start_on_login")]
|
||||
@ -60,6 +65,8 @@ impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
AppConfig {
|
||||
rehide_ms: default_rehide_ms(),
|
||||
auto_lock: default_auto_lock(),
|
||||
lock_after: default_lock_after(),
|
||||
start_minimized: default_start_minimized(),
|
||||
start_on_login: default_start_on_login(),
|
||||
terminal: default_term_config(),
|
||||
@ -187,6 +194,30 @@ fn default_hotkey_config() -> HotkeysConfig {
|
||||
|
||||
|
||||
fn default_rehide_ms() -> u64 { 1000 }
|
||||
fn default_auto_lock() -> bool { true }
|
||||
fn default_lock_after() -> Duration { Duration::from_secs(43200) }
|
||||
// start minimized and on login only in production mode
|
||||
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
|
||||
fn default_start_on_login() -> bool { !cfg!(debug_assertions) }
|
||||
|
||||
|
||||
// struct DurationVisitor;
|
||||
|
||||
// impl<'de> Visitor<'de> for DurationVisitor {
|
||||
// type Value = Duration;
|
||||
|
||||
// fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
|
||||
// write!(formatter, "an integer between 0 and 2^64 - 1")
|
||||
// }
|
||||
|
||||
// fn visit_u64<E: de::Error>(self, v: u64) -> Result<Duration, E> {
|
||||
// Ok(Duration::from_secs(v))
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// fn duration_from_secs<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
||||
// where D: Deserializer<'de>
|
||||
// {
|
||||
// deserializer.deserialize_u64(DurationVisitor)
|
||||
// }
|
||||
|
@ -126,10 +126,10 @@ impl LockedCredentials {
|
||||
let secret_access_key = String::from_utf8(decrypted)
|
||||
.map_err(|_| UnlockError::InvalidUtf8)?;
|
||||
|
||||
let creds = BaseCredentials {
|
||||
access_key_id: self.access_key_id.clone(),
|
||||
let creds = BaseCredentials::new(
|
||||
self.access_key_id.clone(),
|
||||
secret_access_key,
|
||||
};
|
||||
);
|
||||
Ok(creds)
|
||||
}
|
||||
}
|
||||
@ -138,11 +138,16 @@ impl LockedCredentials {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct BaseCredentials {
|
||||
pub version: usize,
|
||||
pub access_key_id: String,
|
||||
pub secret_access_key: String,
|
||||
}
|
||||
|
||||
impl BaseCredentials {
|
||||
pub fn new(access_key_id: String, secret_access_key: String) -> Self {
|
||||
Self {version: 1, access_key_id, secret_access_key}
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, passphrase: &str) -> Result<LockedCredentials, CryptoError> {
|
||||
let salt = Crypto::salt();
|
||||
let crypto = Crypto::new(passphrase, &salt)?;
|
||||
|
@ -18,6 +18,7 @@ use tauri::api::dialog::{
|
||||
MessageDialogBuilder,
|
||||
MessageDialogKind,
|
||||
};
|
||||
use tokio::sync::oneshot::error::RecvError;
|
||||
use serde::{
|
||||
Serialize,
|
||||
Serializer,
|
||||
@ -82,15 +83,33 @@ where
|
||||
}
|
||||
|
||||
|
||||
struct SerializeUpstream<E>(pub E);
|
||||
|
||||
impl<E: Error> Serialize for SerializeUpstream<E> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let msg = format!("{}", self.0);
|
||||
let mut map = serializer.serialize_map(None)?;
|
||||
map.serialize_entry("msg", &msg)?;
|
||||
map.serialize_entry("code", &None::<&str>)?;
|
||||
map.serialize_entry("source", &None::<&str>)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_upstream_err<E, M>(err: &E, map: &mut M) -> Result<(), M::Error>
|
||||
where
|
||||
E: Error,
|
||||
M: serde::ser::SerializeMap,
|
||||
{
|
||||
let msg = err.source().map(|s| format!("{s}"));
|
||||
map.serialize_entry("msg", &msg)?;
|
||||
map.serialize_entry("code", &None::<&str>)?;
|
||||
map.serialize_entry("source", &None::<&str>)?;
|
||||
// let msg = err.source().map(|s| format!("{s}"));
|
||||
// map.serialize_entry("msg", &msg)?;
|
||||
// map.serialize_entry("code", &None::<&str>)?;
|
||||
// map.serialize_entry("source", &None::<&str>)?;
|
||||
|
||||
match err.source() {
|
||||
Some(src) => map.serialize_entry("source", &SerializeUpstream(src))?,
|
||||
None => map.serialize_entry("source", &None::<&str>)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -152,7 +171,7 @@ pub enum SendResponseError {
|
||||
}
|
||||
|
||||
|
||||
// errors encountered while handling an HTTP request
|
||||
// errors encountered while handling a client request
|
||||
#[derive(Debug, ThisError, AsRefStr)]
|
||||
pub enum HandlerError {
|
||||
#[error("Error writing to stream: {0}")]
|
||||
@ -163,8 +182,10 @@ pub enum HandlerError {
|
||||
BadRequest(#[from] serde_json::Error),
|
||||
#[error("HTTP request too large")]
|
||||
RequestTooLarge,
|
||||
#[error("Connection closed early by client")]
|
||||
Abandoned,
|
||||
#[error("Internal server error")]
|
||||
Internal,
|
||||
Internal(#[from] RecvError),
|
||||
#[error("Error accessing credentials: {0}")]
|
||||
NoCredentials(#[from] GetCredentialsError),
|
||||
#[error("Error getting client details: {0}")]
|
||||
@ -226,6 +247,19 @@ pub enum UnlockError {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, ThisError, AsRefStr)]
|
||||
pub enum LockError {
|
||||
#[error("App is not unlocked")]
|
||||
NotUnlocked,
|
||||
#[error("Database error: {0}")]
|
||||
DbError(#[from] SqlxError),
|
||||
#[error(transparent)]
|
||||
Setup(#[from] SetupError),
|
||||
#[error(transparent)]
|
||||
TauriError(#[from] tauri::Error),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, ThisError, AsRefStr)]
|
||||
pub enum CryptoError {
|
||||
#[error(transparent)]
|
||||
@ -344,11 +378,11 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl_serialize_basic!(SetupError);
|
||||
impl_serialize_basic!(GetCredentialsError);
|
||||
impl_serialize_basic!(ClientInfoError);
|
||||
impl_serialize_basic!(WindowError);
|
||||
impl_serialize_basic!(LockError);
|
||||
|
||||
|
||||
impl Serialize for HandlerError {
|
||||
|
@ -21,6 +21,7 @@ pub struct AwsRequestNotification {
|
||||
pub struct RequestResponse {
|
||||
pub id: u64,
|
||||
pub approval: Approval,
|
||||
pub base: bool,
|
||||
}
|
||||
|
||||
|
||||
@ -55,6 +56,13 @@ pub async fn get_session_status(app_state: State<'_, AppState>) -> Result<String
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn signal_activity(app_state: State<'_, AppState>) -> Result<(), ()> {
|
||||
app_state.signal_activity().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_credentials(
|
||||
credentials: BaseCredentials,
|
||||
|
@ -43,6 +43,24 @@ pub enum Response {
|
||||
}
|
||||
|
||||
|
||||
struct CloseWaiter<'s> {
|
||||
stream: &'s mut Stream,
|
||||
}
|
||||
|
||||
impl<'s> CloseWaiter<'s> {
|
||||
async fn wait_for_close(&mut self) -> std::io::Result<()> {
|
||||
let mut buf = [0u8; 8];
|
||||
loop {
|
||||
match self.stream.read(&mut buf).await {
|
||||
Ok(0) => break Ok(()),
|
||||
Ok(_) => (),
|
||||
Err(e) => break Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError>
|
||||
{
|
||||
// read from stream until delimiter is reached
|
||||
@ -59,13 +77,21 @@ async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> R
|
||||
}
|
||||
|
||||
let client = clientinfo::get_process_parent_info(client_pid)?;
|
||||
let waiter = CloseWaiter { stream: &mut stream };
|
||||
|
||||
let req: Request = serde_json::from_slice(&buf)?;
|
||||
let res = match req {
|
||||
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
|
||||
Request::GetAwsCredentials{ base } => get_aws_credentials(
|
||||
base, client, app_handle, waiter
|
||||
).await,
|
||||
Request::InvokeShortcut(action) => invoke_shortcut(action).await,
|
||||
};
|
||||
|
||||
// doesn't make sense to send the error to the client if the client has already left
|
||||
if let Err(HandlerError::Abandoned) = res {
|
||||
return Err(HandlerError::Abandoned);
|
||||
}
|
||||
|
||||
let res = serde_json::to_vec(&res).unwrap();
|
||||
stream.write_all(&res).await?;
|
||||
Ok(())
|
||||
@ -78,7 +104,12 @@ async fn invoke_shortcut(action: ShortcutAction) -> Result<Response, HandlerErro
|
||||
}
|
||||
|
||||
|
||||
async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result<Response, HandlerError> {
|
||||
async fn get_aws_credentials(
|
||||
base: bool,
|
||||
client: Client,
|
||||
app_handle: AppHandle,
|
||||
mut waiter: CloseWaiter<'_>,
|
||||
) -> Result<Response, HandlerError> {
|
||||
let state = app_handle.state::<AppState>();
|
||||
let rehide_ms = {
|
||||
let config = state.config.read().await;
|
||||
@ -97,9 +128,17 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle)
|
||||
let notification = AwsRequestNotification {id: request_id, client, base};
|
||||
app_handle.emit_all("credentials-request", ¬ification)?;
|
||||
|
||||
match chan_recv.await {
|
||||
Ok(Approval::Approved) => {
|
||||
if base {
|
||||
let response = tokio::select! {
|
||||
r = chan_recv => r?,
|
||||
_ = waiter.wait_for_close() => {
|
||||
app_handle.emit_all("request-cancelled", request_id)?;
|
||||
return Err(HandlerError::Abandoned);
|
||||
},
|
||||
};
|
||||
|
||||
match response.approval {
|
||||
Approval::Approved => {
|
||||
if response.base {
|
||||
let creds = state.base_creds_cloned().await?;
|
||||
Ok(Response::Aws(Credentials::Base(creds)))
|
||||
}
|
||||
@ -108,8 +147,7 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle)
|
||||
Ok(Response::Aws(Credentials::Session(creds)))
|
||||
}
|
||||
},
|
||||
Ok(Approval::Denied) => Err(HandlerError::Denied),
|
||||
Err(_e) => Err(HandlerError::Internal),
|
||||
Approval::Denied => Err(HandlerError::Denied),
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use tokio::{
|
||||
sync::RwLock,
|
||||
@ -11,13 +12,14 @@ use tauri::{
|
||||
async_runtime as rt,
|
||||
};
|
||||
|
||||
use crate::app;
|
||||
use crate::credentials::{
|
||||
Session,
|
||||
BaseCredentials,
|
||||
SessionCredentials,
|
||||
};
|
||||
use crate::{config, config::AppConfig};
|
||||
use crate::ipc::{self, Approval};
|
||||
use crate::ipc::{self, Approval, RequestResponse};
|
||||
use crate::errors::*;
|
||||
use crate::shortcuts;
|
||||
|
||||
@ -42,19 +44,19 @@ impl Visibility {
|
||||
// `original` represents the visibility of the window before any leases were acquired
|
||||
// None means we don't know, Some(false) means it was previously hidden,
|
||||
// Some(true) means it was previously visible
|
||||
let is_visible = window.is_visible()?;
|
||||
if self.original.is_none() {
|
||||
let is_visible = window.is_visible()?;
|
||||
self.original = Some(is_visible);
|
||||
}
|
||||
|
||||
let state = app.state::<AppState>();
|
||||
if matches!(self.original, Some(true)) && state.desktop_is_gnome {
|
||||
if is_visible && state.desktop_is_gnome {
|
||||
// Gnome has a really annoying "focus-stealing prevention" behavior means we
|
||||
// can't just pop up when the window is already visible, so to work around it
|
||||
// we hide and then immediately unhide the window
|
||||
window.hide()?;
|
||||
}
|
||||
window.show()?;
|
||||
app::show_main_window(&app)?;
|
||||
window.set_focus()?;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
@ -72,7 +74,7 @@ impl Visibility {
|
||||
visibility.leases -= 1;
|
||||
if visibility.leases == 0 {
|
||||
if let Some(false) = visibility.original {
|
||||
window.hide().error_print();
|
||||
app::hide_main_window(&handle).error_print();
|
||||
}
|
||||
visibility.original = None;
|
||||
}
|
||||
@ -101,8 +103,9 @@ impl VisibilityLease {
|
||||
pub struct AppState {
|
||||
pub config: RwLock<AppConfig>,
|
||||
pub session: RwLock<Session>,
|
||||
pub last_activity: RwLock<OffsetDateTime>,
|
||||
pub request_count: RwLock<u64>,
|
||||
pub waiting_requests: RwLock<HashMap<u64, Sender<Approval>>>,
|
||||
pub waiting_requests: RwLock<HashMap<u64, Sender<RequestResponse>>>,
|
||||
pub pending_terminal_request: RwLock<bool>,
|
||||
// these are never modified and so don't need to be wrapped in RwLocks
|
||||
pub setup_errors: Vec<String>,
|
||||
@ -122,6 +125,7 @@ impl AppState {
|
||||
AppState {
|
||||
config: RwLock::new(config),
|
||||
session: RwLock::new(session),
|
||||
last_activity: RwLock::new(OffsetDateTime::now_utc()),
|
||||
request_count: RwLock::new(0),
|
||||
waiting_requests: RwLock::new(HashMap::new()),
|
||||
pending_terminal_request: RwLock::new(false),
|
||||
@ -161,7 +165,7 @@ impl AppState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn register_request(&self, sender: Sender<Approval>) -> u64 {
|
||||
pub async fn register_request(&self, sender: Sender<RequestResponse>) -> u64 {
|
||||
let count = {
|
||||
let mut c = self.request_count.write().await;
|
||||
*c += 1;
|
||||
@ -193,7 +197,7 @@ impl AppState {
|
||||
waiting_requests
|
||||
.remove(&response.id)
|
||||
.ok_or(SendResponseError::NotFound)?
|
||||
.send(response.approval)
|
||||
.send(response)
|
||||
.map_err(|_| SendResponseError::Abandoned)
|
||||
}
|
||||
|
||||
@ -209,6 +213,38 @@ impl AppState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn lock(&self) -> Result<(), LockError> {
|
||||
let mut session = self.session.write().await;
|
||||
match *session {
|
||||
Session::Empty => Err(LockError::NotUnlocked),
|
||||
Session::Locked(_) => Err(LockError::NotUnlocked),
|
||||
Session::Unlocked{..} => {
|
||||
*session = Session::load(&self.pool).await?;
|
||||
|
||||
let app_handle = app::APP.get().unwrap();
|
||||
app_handle.emit_all("locked", None::<usize>)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn signal_activity(&self) {
|
||||
let mut last_activity = self.last_activity.write().await;
|
||||
*last_activity = OffsetDateTime::now_utc();
|
||||
}
|
||||
|
||||
pub async fn should_auto_lock(&self) -> bool {
|
||||
let config = self.config.read().await;
|
||||
if !config.auto_lock || !self.is_unlocked().await {
|
||||
return false;
|
||||
}
|
||||
|
||||
let last_activity = self.last_activity.read().await;
|
||||
let elapsed = OffsetDateTime::now_utc() - *last_activity;
|
||||
elapsed >= config.lock_after
|
||||
}
|
||||
|
||||
pub async fn is_unlocked(&self) -> bool {
|
||||
let session = self.session.read().await;
|
||||
matches!(*session, Session::Unlocked{..})
|
||||
@ -222,7 +258,7 @@ impl AppState {
|
||||
|
||||
pub async fn session_creds_cloned(&self) -> Result<SessionCredentials, GetCredentialsError> {
|
||||
let app_session = self.session.read().await;
|
||||
let (_bsae, session) = app_session.try_get()?;
|
||||
let (_base, session) = app_session.try_get()?;
|
||||
Ok(session.clone())
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,19 @@
|
||||
use tauri::{
|
||||
AppHandle,
|
||||
CustomMenuItem,
|
||||
Manager,
|
||||
SystemTray,
|
||||
SystemTrayEvent,
|
||||
SystemTrayMenu,
|
||||
CustomMenuItem,
|
||||
async_runtime as rt,
|
||||
};
|
||||
|
||||
use crate::app;
|
||||
use crate::state::AppState;
|
||||
|
||||
|
||||
pub fn create() -> SystemTray {
|
||||
let show = CustomMenuItem::new("show".to_string(), "Show");
|
||||
let show = CustomMenuItem::new("show_hide".to_string(), "Show");
|
||||
let quit = CustomMenuItem::new("exit".to_string(), "Exit");
|
||||
|
||||
let menu = SystemTrayMenu::new()
|
||||
@ -20,13 +24,18 @@ pub fn create() -> SystemTray {
|
||||
}
|
||||
|
||||
|
||||
pub fn handle_event(app: &AppHandle, event: SystemTrayEvent) {
|
||||
pub fn handle_event(app_handle: &AppHandle, event: SystemTrayEvent) {
|
||||
match event {
|
||||
SystemTrayEvent::MenuItemClick{ id, .. } => {
|
||||
match id.as_str() {
|
||||
"exit" => app.exit(0),
|
||||
"show" => {
|
||||
let _ = app.get_window("main").map(|w| w.show());
|
||||
"exit" => app_handle.exit(0),
|
||||
"show_hide" => {
|
||||
let _ = app::toggle_main_window(app_handle);
|
||||
let new_handle = app_handle.app_handle();
|
||||
rt::spawn(async move {
|
||||
let state = new_handle.state::<AppState>();
|
||||
state.signal_activity().await;
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -8,10 +8,15 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "creddy",
|
||||
"version": "0.4.1"
|
||||
"version": "0.4.6"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"app": {
|
||||
"all": true,
|
||||
"show": false,
|
||||
"hide": false
|
||||
},
|
||||
"os": {"all": true},
|
||||
"dialog": {"open": true}
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ import { onMount } from 'svelte';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
import { appState, acceptRequest } from './lib/state.js';
|
||||
import { appState, acceptRequest, cleanupRequest } from './lib/state.js';
|
||||
import { views, currentView, navigate } from './lib/routing.js';
|
||||
|
||||
|
||||
@ -16,6 +16,16 @@ listen('credentials-request', (tauriEvent) => {
|
||||
$appState.pendingRequests.put(tauriEvent.payload);
|
||||
});
|
||||
|
||||
listen('request-cancelled', (tauriEvent) => {
|
||||
const id = tauriEvent.payload;
|
||||
if (id === $appState.currentRequest?.id) {
|
||||
cleanupRequest()
|
||||
}
|
||||
else {
|
||||
const found = $appState.pendingRequests.find_remove(r => r.id === id);
|
||||
}
|
||||
});
|
||||
|
||||
listen('launch-terminal-request', async (tauriEvent) => {
|
||||
if ($appState.currentRequest === null) {
|
||||
let status = await invoke('get_session_status');
|
||||
|
@ -30,5 +30,15 @@ export default function() {
|
||||
|
||||
return this.items.shift();
|
||||
},
|
||||
|
||||
find_remove(pred) {
|
||||
for (let i=0; i<this.items.length; i++) {
|
||||
if (pred(this.items[i])) {
|
||||
this.items.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ export async function acceptRequest() {
|
||||
}
|
||||
|
||||
|
||||
export function completeRequest() {
|
||||
export function cleanupRequest() {
|
||||
appState.update($appState => {
|
||||
$appState.currentRequest = null;
|
||||
return $appState;
|
||||
|
@ -5,3 +5,8 @@
|
||||
.btn-alert-error {
|
||||
@apply bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content
|
||||
}
|
||||
|
||||
/* I like alert icons to be top-aligned */
|
||||
.alert > :where(*) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
<script>
|
||||
export let keys;
|
||||
let classes;
|
||||
export {classes as class};
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex gap-x-[0.2em] items-center">
|
||||
<span class="inline-flex gap-x-[0.2em] items-center {classes}">
|
||||
{#each keys as key, i}
|
||||
{#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>
|
||||
{/each}
|
||||
</div>
|
||||
</span>
|
||||
|
@ -21,15 +21,15 @@
|
||||
throw(`Link target is not a string or a function: ${target}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleHotkey(event) {
|
||||
if (!hotkey) return;
|
||||
if (ctrl && !event.ctrlKey) return;
|
||||
if (alt && !event.altKey) return;
|
||||
if (shift && !event.shiftKey) return;
|
||||
|
||||
if (event.key === hotkey) {
|
||||
if (
|
||||
hotkey === event.key
|
||||
&& ctrl === event.ctrlKey
|
||||
&& alt === event.altKey
|
||||
&& shift === event.shiftKey
|
||||
) {
|
||||
click();
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
import { navigate } from '../lib/routing.js';
|
||||
import { appState, completeRequest } from '../lib/state.js';
|
||||
import { appState, cleanupRequest } from '../lib/state.js';
|
||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||
import Link from '../ui/Link.svelte';
|
||||
import KeyCombo from '../ui/KeyCombo.svelte';
|
||||
@ -12,9 +12,12 @@
|
||||
// Send response to backend, display error if applicable
|
||||
let error, alert;
|
||||
async function respond() {
|
||||
let {id, approval} = $appState.currentRequest;
|
||||
const response = {
|
||||
id: $appState.currentRequest.id,
|
||||
...$appState.currentRequest.response,
|
||||
};
|
||||
try {
|
||||
await invoke('respond', {response: {id, approval}});
|
||||
await invoke('respond', {response});
|
||||
navigate('ShowResponse');
|
||||
}
|
||||
catch (e) {
|
||||
@ -26,8 +29,8 @@
|
||||
}
|
||||
|
||||
// Approval has one of several outcomes depending on current credential state
|
||||
async function approve() {
|
||||
$appState.currentRequest.approval = 'Approved';
|
||||
async function approve(base) {
|
||||
$appState.currentRequest.response = {approval: 'Approved', base};
|
||||
let status = await invoke('get_session_status');
|
||||
if (status === 'unlocked') {
|
||||
await respond();
|
||||
@ -40,9 +43,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function approve_base() {
|
||||
approve(true);
|
||||
}
|
||||
function approve_session() {
|
||||
approve(false);
|
||||
}
|
||||
|
||||
// Denial has only one
|
||||
async function deny() {
|
||||
$appState.currentRequest.approval = 'Denied';
|
||||
$appState.currentRequest.response = {approval: 'Denied', base: false};
|
||||
await respond();
|
||||
}
|
||||
|
||||
@ -58,32 +68,32 @@
|
||||
|
||||
// if the request has already been approved/denied, send response immediately
|
||||
onMount(async () => {
|
||||
if ($appState.currentRequest.approval) {
|
||||
if ($appState.currentRequest.response) {
|
||||
await respond();
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Don't render at all if we're just going to immediately proceed to the next screen -->
|
||||
{#if error || !$appState.currentRequest.approval}
|
||||
{#if error || !$appState.currentRequest?.response}
|
||||
<div class="flex flex-col space-y-4 p-4 m-auto max-w-xl h-screen items-center justify-center">
|
||||
{#if error}
|
||||
<ErrorAlert bind:this={alert}>
|
||||
{error}
|
||||
{error.msg}
|
||||
<svelte:fragment slot="buttons">
|
||||
<button class="btn btn-sm btn-alert-error" on:click={completeRequest}>Cancel</button>
|
||||
<button class="btn btn-sm btn-alert-error" on:click={cleanupRequest}>Cancel</button>
|
||||
<button class="btn btn-sm btn-alert-error" on:click={respond}>Retry</button>
|
||||
</svelte:fragment>
|
||||
</ErrorAlert>
|
||||
{/if}
|
||||
|
||||
{#if $appState.currentRequest.base}
|
||||
{#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 (long-lived) AWS credentials.
|
||||
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>
|
||||
@ -101,20 +111,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between">
|
||||
<Link target={deny} hotkey="Escape">
|
||||
<button class="btn btn-error justify-self-start">
|
||||
<span class="mr-2">Deny</span>
|
||||
<KeyCombo keys={['Esc']} />
|
||||
</button>
|
||||
</Link>
|
||||
<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={() => approve(false)} hotkey="Enter" shift={true}>
|
||||
<button class="w-full btn btn-success">
|
||||
<KeyCombo keys={['Shift', 'Enter']} />
|
||||
</button>
|
||||
</Link>
|
||||
{/if}
|
||||
|
||||
<Link target={approve} hotkey="Enter" shift="{true}">
|
||||
<button class="btn btn-success justify-self-end">
|
||||
<span class="mr-2">Approve</span>
|
||||
<KeyCombo keys={['Shift', 'Enter']} />
|
||||
</button>
|
||||
</Link>
|
||||
<h3 class="font-semibold">
|
||||
<span class="mr-2">
|
||||
{#if $appState.currentRequest?.base}
|
||||
Approve
|
||||
{:else}
|
||||
Approve with base credentials
|
||||
{/if}
|
||||
</span>
|
||||
</h3>
|
||||
<Link target={() => approve(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={deny} hotkey="Escape">
|
||||
<button class="w-full btn btn-error">
|
||||
<KeyCombo keys={['Esc']} />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -39,8 +39,8 @@
|
||||
Launch Terminal
|
||||
</button>
|
||||
<label class="label cursor-pointer flex items-center space-x-2">
|
||||
<span class="label-text">Launch with long-lived credentials</span>
|
||||
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={launchBase}>
|
||||
<span class="label-text">Launch with base credentials</span>
|
||||
</label>
|
||||
|
||||
{:else if status === 'empty'}
|
||||
|
@ -112,7 +112,7 @@
|
||||
|
||||
<div>
|
||||
<!-- <button class="btn btn-sm btn-ghost">Cancel</button> -->
|
||||
<buton class="btn btn-sm btn-primary" on:click={save}>Save</buton>
|
||||
<button class="btn btn-sm btn-primary" on:click={save}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { draw, fade } from 'svelte/transition';
|
||||
|
||||
import { appState, completeRequest } from '../lib/state.js';
|
||||
import { appState, cleanupRequest } from '../lib/state.js';
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
onMount(() => {
|
||||
window.setTimeout(
|
||||
completeRequest,
|
||||
cleanupRequest,
|
||||
// Extra 50ms so the window can finish disappearing before the redraw
|
||||
Math.min(5000, $appState.config.rehide_ms + 50),
|
||||
)
|
||||
@ -22,7 +22,7 @@
|
||||
|
||||
|
||||
<div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
|
||||
{#if $appState.currentRequest.approval === 'Approved'}
|
||||
{#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>
|
||||
@ -33,6 +33,6 @@
|
||||
{/if}
|
||||
|
||||
<div in:fade="{{duration: fadeDuration, delay: fadeDelay}}" class="text-2xl font-bold">
|
||||
{$appState.currentRequest.approval}!
|
||||
{$appState.currentRequest.response.approval}!
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user