Compare commits
3 Commits
41f8e8f2ab
...
0d37814cf4
Author | SHA1 | Date | |
---|---|---|---|
0d37814cf4 | |||
31532cd76e | |||
|
a8aafa1519 |
2272
src-tauri/Cargo.lock
generated
2272
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -17,8 +17,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", "os-all", "system-tray"] }
|
tauri = { version = "1.0.5", features = ["api-all", "system-tray"] }
|
||||||
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"] }
|
||||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
|
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||||
@ -32,16 +31,14 @@ thiserror = "1.0.38"
|
|||||||
once_cell = "1.16.0"
|
once_cell = "1.16.0"
|
||||||
strum = "0.24"
|
strum = "0.24"
|
||||||
strum_macros = "0.24"
|
strum_macros = "0.24"
|
||||||
auto-launch = "0.4.0"
|
|
||||||
dirs = "5.0"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||||
default = ["custom-protocol"]
|
default = [ "custom-protocol" ]
|
||||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||||
# DO NOT remove this
|
# DO NOT remove this
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
|
|
||||||
# [profile.dev.build-override]
|
# [profile.dev.build-override]
|
||||||
# opt-level = 3
|
# opt-level = 3
|
||||||
|
@ -8,7 +8,7 @@ CREATE TABLE credentials (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE config (
|
CREATE TABLE config (
|
||||||
name TEXT UNIQUE NOT NULL,
|
name TEXT NOT NULL,
|
||||||
data TEXT NOT NULL
|
data TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
|
use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
|
||||||
use tauri::Manager;
|
|
||||||
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
|
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::errors::*;
|
||||||
errors::*,
|
use crate::get_state;
|
||||||
config::AppConfig,
|
|
||||||
state::AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||||
@ -17,18 +13,13 @@ pub struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
|
fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
|
||||||
let state = crate::APP.get().unwrap().state::<AppState>();
|
|
||||||
let AppConfig {
|
|
||||||
listen_addr: app_listen_addr,
|
|
||||||
listen_port: app_listen_port,
|
|
||||||
..
|
|
||||||
} = *state.config.read().await;
|
|
||||||
|
|
||||||
let sockets_iter = netstat2::iterate_sockets_info(
|
let sockets_iter = netstat2::iterate_sockets_info(
|
||||||
AddressFamilyFlags::IPV4,
|
AddressFamilyFlags::IPV4,
|
||||||
ProtocolFlags::TCP
|
ProtocolFlags::TCP
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
get_state!(config as app_config);
|
||||||
for item in sockets_iter {
|
for item in sockets_iter {
|
||||||
let sock_info = item?;
|
let sock_info = item?;
|
||||||
let proto_info = match sock_info.protocol_socket_info {
|
let proto_info = match sock_info.protocol_socket_info {
|
||||||
@ -37,9 +28,9 @@ async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::erro
|
|||||||
};
|
};
|
||||||
|
|
||||||
if proto_info.local_port == local_port
|
if proto_info.local_port == local_port
|
||||||
&& proto_info.remote_port == app_listen_port
|
&& proto_info.remote_port == app_config.listen_port
|
||||||
&& proto_info.local_addr == app_listen_addr
|
&& proto_info.local_addr == app_config.listen_addr
|
||||||
&& proto_info.remote_addr == app_listen_addr
|
&& proto_info.remote_addr == app_config.listen_addr
|
||||||
{
|
{
|
||||||
return Ok(sock_info.associated_pids)
|
return Ok(sock_info.associated_pids)
|
||||||
}
|
}
|
||||||
@ -49,10 +40,10 @@ async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::erro
|
|||||||
|
|
||||||
|
|
||||||
// Theoretically, on some systems, multiple processes can share a socket
|
// Theoretically, on some systems, multiple processes can share a socket
|
||||||
pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
|
pub fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
|
||||||
let mut clients = Vec::new();
|
let mut clients = Vec::new();
|
||||||
let mut sys = System::new();
|
let mut sys = System::new();
|
||||||
for p in get_associated_pids(local_port).await? {
|
for p in get_associated_pids(local_port)? {
|
||||||
let pid = Pid::from_u32(p);
|
let pid = Pid::from_u32(p);
|
||||||
sys.refresh_process(pid);
|
sys.refresh_process(pid);
|
||||||
let proc = sys.process(pid)
|
let proc = sys.process(pid)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use auto_launch::AutoLaunchBuilder;
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ pub struct AppConfig {
|
|||||||
pub rehide_ms: u64,
|
pub rehide_ms: u64,
|
||||||
#[serde(default = "default_start_minimized")]
|
#[serde(default = "default_start_minimized")]
|
||||||
pub start_minimized: bool,
|
pub start_minimized: bool,
|
||||||
#[serde(default = "default_start_on_login")]
|
|
||||||
pub start_on_login: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -30,14 +27,12 @@ impl Default for AppConfig {
|
|||||||
listen_port: default_listen_port(),
|
listen_port: default_listen_port(),
|
||||||
rehide_ms: default_rehide_ms(),
|
rehide_ms: default_rehide_ms(),
|
||||||
start_minimized: default_start_minimized(),
|
start_minimized: default_start_minimized(),
|
||||||
start_on_login: default_start_on_login(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl AppConfig {
|
pub async fn load(pool: &SqlitePool) -> Result<AppConfig, SetupError> {
|
||||||
pub async fn load(pool: &SqlitePool) -> Result<AppConfig, SetupError> {
|
|
||||||
let res = sqlx::query!("SELECT * from config where name = 'main'")
|
let res = sqlx::query!("SELECT * from config where name = 'main'")
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?;
|
.await?;
|
||||||
@ -48,60 +43,24 @@ impl AppConfig {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(serde_json::from_str(&row.data)?)
|
Ok(serde_json::from_str(&row.data)?)
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
|
|
||||||
let data = serde_json::to_string(self).unwrap();
|
|
||||||
sqlx::query(
|
|
||||||
"INSERT INTO config (name, data) VALUES ('main', ?)
|
|
||||||
ON CONFLICT (name) DO UPDATE SET data = ?"
|
|
||||||
)
|
|
||||||
.bind(&data)
|
|
||||||
.bind(&data)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn set_auto_launch(is_configured: bool) -> Result<(), SetupError> {
|
pub fn get_or_create_db_path() -> PathBuf {
|
||||||
let path_buf = std::env::current_exe()
|
if cfg!(debug_assertions) {
|
||||||
.map_err(|e| auto_launch::Error::Io(e))?;
|
return PathBuf::from("./creddy.db");
|
||||||
let path = path_buf
|
|
||||||
.to_string_lossy();
|
|
||||||
|
|
||||||
let auto = AutoLaunchBuilder::new()
|
|
||||||
.set_app_name("Creddy")
|
|
||||||
.set_app_path(&path)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let is_enabled = auto.is_enabled()?;
|
|
||||||
if is_configured && !is_enabled {
|
|
||||||
auto.enable()?;
|
|
||||||
}
|
|
||||||
else if !is_configured && is_enabled {
|
|
||||||
auto.disable()?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let mut parent = std::env::var("HOME")
|
||||||
}
|
.map(|h| {
|
||||||
|
let mut p = PathBuf::from(h);
|
||||||
|
p.push(".config");
|
||||||
|
p
|
||||||
|
})
|
||||||
|
.unwrap_or(PathBuf::from("."));
|
||||||
|
|
||||||
|
parent.push("creddy.db");
|
||||||
pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
|
parent
|
||||||
// debug_assertions doesn't always mean we are running in dev
|
|
||||||
if cfg!(debug_assertions) && std::env::var("HOME").is_ok() {
|
|
||||||
return Ok(PathBuf::from("./creddy.db"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut path = dirs::data_dir()
|
|
||||||
.ok_or(DataDirError::NotFound)?;
|
|
||||||
|
|
||||||
std::fs::create_dir_all(&path)?;
|
|
||||||
path.push("creddy.db");
|
|
||||||
|
|
||||||
Ok(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -115,7 +74,7 @@ fn default_listen_port() -> u16 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
|
fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
|
||||||
|
|
||||||
fn default_rehide_ms() -> u64 { 1000 }
|
fn default_rehide_ms() -> u64 { 1000 }
|
||||||
// start minimized and on login only in production mode
|
|
||||||
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
|
fn default_start_minimized() -> bool { !cfg!(debug_assertions) } // default to start-minimized in production only
|
||||||
fn default_start_on_login() -> bool { !cfg!(debug_assertions) }
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::convert::AsRef;
|
use std::convert::AsRef;
|
||||||
use std::sync::mpsc;
|
|
||||||
use strum_macros::AsRefStr;
|
use strum_macros::AsRefStr;
|
||||||
|
|
||||||
use thiserror::Error as ThisError;
|
use thiserror::Error as ThisError;
|
||||||
|
|
||||||
use aws_sdk_sts::{
|
use aws_sdk_sts::{
|
||||||
types::SdkError as AwsSdkError,
|
types::SdkError as AwsSdkError,
|
||||||
error::GetSessionTokenError,
|
error::GetSessionTokenError,
|
||||||
@ -12,29 +12,32 @@ use sqlx::{
|
|||||||
error::Error as SqlxError,
|
error::Error as SqlxError,
|
||||||
migrate::MigrateError,
|
migrate::MigrateError,
|
||||||
};
|
};
|
||||||
use tauri::api::dialog::{
|
|
||||||
MessageDialogBuilder,
|
|
||||||
MessageDialogKind,
|
|
||||||
};
|
|
||||||
use serde::{Serialize, Serializer, ser::SerializeMap};
|
use serde::{Serialize, Serializer, ser::SerializeMap};
|
||||||
|
|
||||||
|
|
||||||
pub trait ErrorPopup {
|
// pub struct SerializeError<E> {
|
||||||
fn error_popup(self, title: &str);
|
// pub err: E,
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl<E: Error> ErrorPopup for Result<(), E> {
|
// impl<E: std::error::Error> Serialize for SerializeError<E>
|
||||||
fn error_popup(self, title: &str) {
|
// {
|
||||||
if let Err(e) = self {
|
// fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
let (tx, rx) = mpsc::channel();
|
// let mut map = serializer.serialize_map(None)?;
|
||||||
MessageDialogBuilder::new(title, format!("{e}"))
|
// map.serialize_entry("msg", &format!("{}", self.err))?;
|
||||||
.kind(MessageDialogKind::Error)
|
// if let Some(src) = self.err.source() {
|
||||||
.show(move |_| tx.send(true).unwrap());
|
// let ser_src = SerializeError { err: src };
|
||||||
|
// map.serialize_entry("source", &ser_src)?;
|
||||||
|
// }
|
||||||
|
// map.end()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
rx.recv().unwrap();
|
// impl<E: std::error::Error> From<E> for SerializeError<E> {
|
||||||
}
|
// fn from(err: E) -> Self {
|
||||||
}
|
// SerializeError { err }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
fn serialize_basic_err<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize_basic_err<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
@ -84,21 +87,6 @@ pub enum SetupError {
|
|||||||
MigrationError(#[from] MigrateError),
|
MigrationError(#[from] MigrateError),
|
||||||
#[error("Error parsing configuration from database")]
|
#[error("Error parsing configuration from database")]
|
||||||
ConfigParseError(#[from] serde_json::Error),
|
ConfigParseError(#[from] serde_json::Error),
|
||||||
#[error("Failed to set up start-on-login: {0}")]
|
|
||||||
AutoLaunchError(#[from] auto_launch::Error),
|
|
||||||
#[error("Failed to start listener: {0}")]
|
|
||||||
ServerSetupError(#[from] std::io::Error),
|
|
||||||
#[error("Failed to resolve data directory: {0}")]
|
|
||||||
DataDir(#[from] DataDirError),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, ThisError, AsRefStr)]
|
|
||||||
pub enum DataDirError {
|
|
||||||
#[error("Could not determine data directory")]
|
|
||||||
NotFound,
|
|
||||||
#[error("Failed to create data directory: {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -106,11 +94,9 @@ pub enum DataDirError {
|
|||||||
#[derive(Debug, ThisError, AsRefStr)]
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
pub enum SendResponseError {
|
pub enum SendResponseError {
|
||||||
#[error("The specified credentials request was not found")]
|
#[error("The specified credentials request was not found")]
|
||||||
NotFound,
|
NotFound, // no request with the given id
|
||||||
#[error("The specified request was already closed by the client")]
|
#[error("The specified request was already closed by the client")]
|
||||||
Abandoned,
|
Abandoned, // request has already been closed by client
|
||||||
#[error("Could not renew AWS sesssion: {0}")]
|
|
||||||
SessionRenew(#[from] GetSessionError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -121,8 +107,7 @@ pub enum RequestError {
|
|||||||
StreamIOError(#[from] std::io::Error),
|
StreamIOError(#[from] std::io::Error),
|
||||||
// #[error("Received invalid UTF-8 in request")]
|
// #[error("Received invalid UTF-8 in request")]
|
||||||
// InvalidUtf8,
|
// InvalidUtf8,
|
||||||
#[error("HTTP request malformed")]
|
// MalformedHttpRequest,
|
||||||
BadRequest,
|
|
||||||
#[error("HTTP request too large")]
|
#[error("HTTP request too large")]
|
||||||
RequestTooLarge,
|
RequestTooLarge,
|
||||||
#[error("Error accessing credentials: {0}")]
|
#[error("Error accessing credentials: {0}")]
|
||||||
@ -148,13 +133,9 @@ pub enum GetCredentialsError {
|
|||||||
#[derive(Debug, ThisError, AsRefStr)]
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
pub enum GetSessionError {
|
pub enum GetSessionError {
|
||||||
#[error("Request completed successfully but no credentials were returned")]
|
#[error("Request completed successfully but no credentials were returned")]
|
||||||
EmptyResponse, // SDK returned successfully but credentials are None
|
NoCredentials, // SDK returned successfully but credentials are None
|
||||||
#[error("Error response from AWS SDK: {0}")]
|
#[error("Error response from AWS SDK: {0}")]
|
||||||
SdkError(#[from] AwsSdkError<GetSessionTokenError>),
|
SdkError(#[from] AwsSdkError<GetSessionTokenError>),
|
||||||
#[error("Could not construt session: credentials are locked")]
|
|
||||||
CredentialsLocked,
|
|
||||||
#[error("Could not construct session: no credentials are known")]
|
|
||||||
CredentialsEmpty,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -206,6 +187,7 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> {
|
|||||||
|
|
||||||
|
|
||||||
impl_serialize_basic!(SetupError);
|
impl_serialize_basic!(SetupError);
|
||||||
|
impl_serialize_basic!(SendResponseError);
|
||||||
impl_serialize_basic!(GetCredentialsError);
|
impl_serialize_basic!(GetCredentialsError);
|
||||||
impl_serialize_basic!(ClientInfoError);
|
impl_serialize_basic!(ClientInfoError);
|
||||||
|
|
||||||
@ -227,22 +209,6 @@ impl Serialize for RequestError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Serialize for SendResponseError {
|
|
||||||
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 {
|
|
||||||
SendResponseError::SessionRenew(src) => map.serialize_entry("source", &src)?,
|
|
||||||
_ => serialize_upstream_err(self, &mut map)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
map.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl Serialize for GetSessionError {
|
impl Serialize for GetSessionError {
|
||||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
let mut map = serializer.serialize_map(None)?;
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
@ -4,7 +4,7 @@ use tauri::State;
|
|||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::clientinfo::Client;
|
use crate::clientinfo::Client;
|
||||||
use crate::state::{AppState, Session, BaseCredentials};
|
use crate::state::{AppState, Session, Credentials};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
@ -29,32 +29,32 @@ pub enum Approval {
|
|||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn respond(response: RequestResponse, app_state: State<'_, AppState>) -> Result<(), SendResponseError> {
|
pub fn respond(response: RequestResponse, app_state: State<'_, AppState>) -> Result<(), String> {
|
||||||
app_state.send_response(response).await
|
app_state.send_response(response)
|
||||||
|
.map_err(|e| format!("Error responding to request: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), UnlockError> {
|
pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), UnlockError> {
|
||||||
app_state.unlock(&passphrase).await
|
app_state.decrypt(&passphrase).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_session_status(app_state: State<'_, AppState>) -> Result<String, ()> {
|
pub fn get_session_status(app_state: State<'_, AppState>) -> String {
|
||||||
let session = app_state.session.read().await;
|
let session = app_state.session.read().unwrap();
|
||||||
let status = match *session {
|
match *session {
|
||||||
Session::Locked(_) => "locked".into(),
|
Session::Locked(_) => "locked".into(),
|
||||||
Session::Unlocked{..} => "unlocked".into(),
|
Session::Unlocked(_) => "unlocked".into(),
|
||||||
Session::Empty => "empty".into()
|
Session::Empty => "empty".into()
|
||||||
};
|
}
|
||||||
Ok(status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_credentials(
|
pub async fn save_credentials(
|
||||||
credentials: BaseCredentials,
|
credentials: Credentials,
|
||||||
passphrase: String,
|
passphrase: String,
|
||||||
app_state: State<'_, AppState>
|
app_state: State<'_, AppState>
|
||||||
) -> Result<(), UnlockError> {
|
) -> Result<(), UnlockError> {
|
||||||
@ -63,16 +63,14 @@ pub async fn save_credentials(
|
|||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
|
pub fn get_config(app_state: State<'_, AppState>) -> AppConfig {
|
||||||
let config = app_state.config.read().await;
|
let config = app_state.config.read().unwrap();
|
||||||
Ok(config.clone())
|
config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> Result<(), String> {
|
pub fn save_config(config: AppConfig, app_state: State<'_, AppState>) {
|
||||||
app_state.update_config(config)
|
let mut prev_config = app_state.config.write().unwrap();
|
||||||
.await
|
*prev_config = config;
|
||||||
.map_err(|e| format!("Error saving config: {e}"))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,9 @@
|
|||||||
all(not(debug_assertions), target_os = "windows"),
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
|
use tauri::{AppHandle, Manager, async_runtime as rt};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use sqlx::{
|
|
||||||
SqlitePool,
|
|
||||||
sqlite::SqlitePoolOptions,
|
|
||||||
sqlite::SqliteConnectOptions,
|
|
||||||
};
|
|
||||||
use tauri::{
|
|
||||||
App,
|
|
||||||
AppHandle,
|
|
||||||
Manager,
|
|
||||||
async_runtime as rt,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod errors;
|
mod errors;
|
||||||
@ -25,48 +14,20 @@ mod state;
|
|||||||
mod server;
|
mod server;
|
||||||
mod tray;
|
mod tray;
|
||||||
|
|
||||||
use config::AppConfig;
|
use crate::errors::*;
|
||||||
use server::Server;
|
|
||||||
use errors::*;
|
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
|
||||||
|
|
||||||
pub static APP: OnceCell<AppHandle> = OnceCell::new();
|
pub static APP: OnceCell<AppHandle> = OnceCell::new();
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let initial_state = match rt::block_on(state::AppState::load()) {
|
||||||
|
Ok(state) => state,
|
||||||
|
Err(e) => {eprintln!("{}", e); return;}
|
||||||
|
};
|
||||||
|
|
||||||
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
|
||||||
APP.set(app.handle()).unwrap();
|
|
||||||
|
|
||||||
let conn_opts = SqliteConnectOptions::new()
|
|
||||||
.filename(config::get_or_create_db_path()?)
|
|
||||||
.create_if_missing(true);
|
|
||||||
let pool_opts = SqlitePoolOptions::new();
|
|
||||||
let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?;
|
|
||||||
sqlx::migrate!().run(&pool).await?;
|
|
||||||
|
|
||||||
let conf = AppConfig::load(&pool).await?;
|
|
||||||
let session = AppState::load_creds(&pool).await?;
|
|
||||||
let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?;
|
|
||||||
|
|
||||||
config::set_auto_launch(conf.start_on_login)?;
|
|
||||||
if !conf.start_minimized {
|
|
||||||
app.get_window("main")
|
|
||||||
.ok_or(RequestError::NoMainWindow)?
|
|
||||||
.show()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = AppState::new(conf, session, srv, pool);
|
|
||||||
app.manage(state);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn run() -> tauri::Result<()> {
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
|
.manage(initial_state)
|
||||||
app.get_window("main")
|
|
||||||
.map(|w| w.show().error_popup("Failed to show main window"));
|
|
||||||
}))
|
|
||||||
.system_tray(tray::create())
|
.system_tray(tray::create())
|
||||||
.on_system_tray_event(tray::handle_event)
|
.on_system_tray_event(tray::handle_event)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
@ -77,8 +38,22 @@ fn run() -> tauri::Result<()> {
|
|||||||
ipc::get_config,
|
ipc::get_config,
|
||||||
ipc::save_config,
|
ipc::save_config,
|
||||||
])
|
])
|
||||||
.setup(|app| rt::block_on(setup(app)))
|
.setup(|app| {
|
||||||
.build(tauri::generate_context!())?
|
APP.set(app.handle()).unwrap();
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
let config = state.config.read().unwrap();
|
||||||
|
let addr = std::net::SocketAddrV4::new(config.listen_addr, config.listen_port);
|
||||||
|
tauri::async_runtime::spawn(server::serve(addr, app.handle()));
|
||||||
|
|
||||||
|
if !config.start_minimized {
|
||||||
|
app.get_window("main")
|
||||||
|
.ok_or(RequestError::NoMainWindow)?
|
||||||
|
.show()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.build(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application")
|
||||||
.run(|app, run_event| match run_event {
|
.run(|app, run_event| match run_event {
|
||||||
tauri::RunEvent::WindowEvent { label, event, .. } => match event {
|
tauri::RunEvent::WindowEvent { label, event, .. } => match event {
|
||||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||||
@ -88,12 +63,39 @@ fn run() -> tauri::Result<()> {
|
|||||||
_ => ()
|
_ => ()
|
||||||
}
|
}
|
||||||
_ => ()
|
_ => ()
|
||||||
});
|
})
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
macro_rules! get_state {
|
||||||
run().error_popup("Creddy failed to start");
|
($prop:ident as $name:ident) => {
|
||||||
|
use tauri::Manager;
|
||||||
|
let app = crate::APP.get().unwrap(); // as long as the app is running, this is fine
|
||||||
|
let state = app.state::<crate::state::AppState>();
|
||||||
|
let $name = state.$prop.read().unwrap(); // only panics if another thread has already panicked
|
||||||
|
};
|
||||||
|
(config.$prop:ident as $name:ident) => {
|
||||||
|
use tauri::Manager;
|
||||||
|
let app = crate::APP.get().unwrap();
|
||||||
|
let state = app.state::<crate::state::AppState>();
|
||||||
|
let config = state.config.read().unwrap();
|
||||||
|
let $name = config.$prop;
|
||||||
|
};
|
||||||
|
|
||||||
|
(mut $prop:ident as $name:ident) => {
|
||||||
|
use tauri::Manager;
|
||||||
|
let app = crate::APP.get().unwrap();
|
||||||
|
let state = app.state::<crate::state::AppState>();
|
||||||
|
let $name = state.$prop.write().unwrap();
|
||||||
|
};
|
||||||
|
(mut config.$prop:ident as $name:ident) => {
|
||||||
|
use tauri::Manager;
|
||||||
|
let app = crate::APP.get().unwrap();
|
||||||
|
let state = app.state::<crate::state::AppState>();
|
||||||
|
let config = state.config.write().unwrap();
|
||||||
|
let $name = config.$prop;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub(crate) use get_state;
|
||||||
|
@ -1,21 +1,12 @@
|
|||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::net::{
|
use std::net::{SocketAddr, SocketAddrV4};
|
||||||
Ipv4Addr,
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
SocketAddr,
|
|
||||||
SocketAddrV4,
|
|
||||||
};
|
|
||||||
use tokio::net::{
|
|
||||||
TcpListener,
|
|
||||||
TcpStream,
|
|
||||||
};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
use tauri::async_runtime as rt;
|
|
||||||
use tauri::async_runtime::JoinHandle;
|
|
||||||
|
|
||||||
use crate::{clientinfo, clientinfo::Client};
|
use crate::{clientinfo, clientinfo::Client};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -31,10 +22,10 @@ struct Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Handler {
|
impl Handler {
|
||||||
async fn new(stream: TcpStream, app: AppHandle) -> Self {
|
fn new(stream: TcpStream, app: AppHandle) -> Self {
|
||||||
let state = app.state::<AppState>();
|
let state = app.state::<AppState>();
|
||||||
let (chan_send, chan_recv) = oneshot::channel();
|
let (chan_send, chan_recv) = oneshot::channel();
|
||||||
let request_id = state.register_request(chan_send).await;
|
let request_id = state.register_request(chan_send);
|
||||||
Handler {
|
Handler {
|
||||||
request_id,
|
request_id,
|
||||||
stream,
|
stream,
|
||||||
@ -48,13 +39,13 @@ impl Handler {
|
|||||||
eprintln!("{e}");
|
eprintln!("{e}");
|
||||||
}
|
}
|
||||||
let state = self.app.state::<AppState>();
|
let state = self.app.state::<AppState>();
|
||||||
state.unregister_request(self.request_id).await;
|
state.unregister_request(self.request_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_handle(&mut self) -> Result<(), RequestError> {
|
async fn try_handle(&mut self) -> Result<(), RequestError> {
|
||||||
let _ = self.recv_request().await?;
|
let _ = self.recv_request().await?;
|
||||||
let clients = self.get_clients().await?;
|
let clients = self.get_clients()?;
|
||||||
if self.includes_banned(&clients).await {
|
if self.includes_banned(&clients) {
|
||||||
self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
|
self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
|
||||||
return Ok(())
|
return Ok(())
|
||||||
}
|
}
|
||||||
@ -68,7 +59,7 @@ impl Handler {
|
|||||||
Approval::Denied => {
|
Approval::Denied => {
|
||||||
let state = self.app.state::<AppState>();
|
let state = self.app.state::<AppState>();
|
||||||
for client in req.clients {
|
for client in req.clients {
|
||||||
state.add_ban(client).await;
|
state.add_ban(client, self.app.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,12 +68,12 @@ impl Handler {
|
|||||||
// and b) there are no other pending requests
|
// and b) there are no other pending requests
|
||||||
let state = self.app.state::<AppState>();
|
let state = self.app.state::<AppState>();
|
||||||
let delay = {
|
let delay = {
|
||||||
let config = state.config.read().await;
|
let config = state.config.read().unwrap();
|
||||||
Duration::from_millis(config.rehide_ms)
|
Duration::from_millis(config.rehide_ms)
|
||||||
};
|
};
|
||||||
sleep(delay).await;
|
sleep(delay).await;
|
||||||
|
|
||||||
if !starting_visibility && state.req_count().await == 0 {
|
if !starting_visibility && state.req_count() == 0 {
|
||||||
let window = self.app.get_window("main").ok_or(RequestError::NoMainWindow)?;
|
let window = self.app.get_window("main").ok_or(RequestError::NoMainWindow)?;
|
||||||
window.hide()?;
|
window.hide()?;
|
||||||
}
|
}
|
||||||
@ -103,31 +94,21 @@ impl Handler {
|
|||||||
println!("{}", std::str::from_utf8(&buf).unwrap());
|
println!("{}", std::str::from_utf8(&buf).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = buf.split(|&c| &[c] == b" ")
|
|
||||||
.skip(1)
|
|
||||||
.next()
|
|
||||||
.ok_or(RequestError::BadRequest(buf))?;
|
|
||||||
|
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_clients(&self) -> Result<Vec<Option<Client>>, RequestError> {
|
fn get_clients(&self) -> Result<Vec<Option<Client>>, RequestError> {
|
||||||
let peer_addr = match self.stream.peer_addr()? {
|
let peer_addr = match self.stream.peer_addr()? {
|
||||||
SocketAddr::V4(addr) => addr,
|
SocketAddr::V4(addr) => addr,
|
||||||
_ => unreachable!(), // we only listen on IPv4
|
_ => unreachable!(), // we only listen on IPv4
|
||||||
};
|
};
|
||||||
let clients = clientinfo::get_clients(peer_addr.port()).await?;
|
let clients = clientinfo::get_clients(peer_addr.port())?;
|
||||||
Ok(clients)
|
Ok(clients)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn includes_banned(&self, clients: &Vec<Option<Client>>) -> bool {
|
fn includes_banned(&self, clients: &Vec<Option<Client>>) -> bool {
|
||||||
let state = self.app.state::<AppState>();
|
let state = self.app.state::<AppState>();
|
||||||
for client in clients {
|
clients.iter().any(|c| state.is_banned(c))
|
||||||
if state.is_banned(client).await {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_window(&self) -> Result<bool, RequestError> {
|
fn show_window(&self) -> Result<bool, RequestError> {
|
||||||
@ -166,7 +147,7 @@ impl Handler {
|
|||||||
|
|
||||||
async fn send_credentials(&mut self) -> Result<(), RequestError> {
|
async fn send_credentials(&mut self) -> Result<(), RequestError> {
|
||||||
let state = self.app.state::<AppState>();
|
let state = self.app.state::<AppState>();
|
||||||
let creds = state.serialize_session_creds().await?;
|
let creds = state.get_creds_serialized()?;
|
||||||
|
|
||||||
self.stream.write(b"\r\nContent-Length: ").await?;
|
self.stream.write(b"\r\nContent-Length: ").await?;
|
||||||
self.stream.write(creds.as_bytes().len().to_string().as_bytes()).await?;
|
self.stream.write(creds.as_bytes().len().to_string().as_bytes()).await?;
|
||||||
@ -178,56 +159,18 @@ impl Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub async fn serve(addr: SocketAddrV4, app_handle: AppHandle) -> io::Result<()> {
|
||||||
pub struct Server {
|
let listener = TcpListener::bind(&addr).await?;
|
||||||
addr: Ipv4Addr,
|
println!("Listening on {addr}");
|
||||||
port: u16,
|
|
||||||
app_handle: AppHandle,
|
|
||||||
task: JoinHandle<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
pub async fn new(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<Server> {
|
|
||||||
let task = Self::start_server(addr, port, app_handle.app_handle()).await?;
|
|
||||||
Ok(Server { addr, port, app_handle, task})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn rebind(&mut self, addr: Ipv4Addr, port: u16) -> io::Result<()> {
|
|
||||||
if addr == self.addr && port == self.port {
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_task = Self::start_server(addr, port, self.app_handle.app_handle()).await?;
|
|
||||||
self.task.abort();
|
|
||||||
|
|
||||||
self.addr = addr;
|
|
||||||
self.port = port;
|
|
||||||
self.task = new_task;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct the listener before spawning the task so that we can return early if it fails
|
|
||||||
async fn start_server(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<JoinHandle<()>> {
|
|
||||||
let sock_addr = SocketAddrV4::new(addr, port);
|
|
||||||
let listener = TcpListener::bind(&sock_addr).await?;
|
|
||||||
let task = rt::spawn(
|
|
||||||
Self::serve(listener, app_handle.app_handle())
|
|
||||||
);
|
|
||||||
Ok(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serve(listener: TcpListener, app_handle: AppHandle) {
|
|
||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
Ok((stream, _)) => {
|
Ok((stream, _)) => {
|
||||||
let handler = Handler::new(stream, app_handle.app_handle()).await;
|
let handler = Handler::new(stream, app_handle.app_handle());
|
||||||
rt::spawn(handler.handle());
|
tauri::async_runtime::spawn(handler.handle());
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error accepting connection: {e}");
|
eprintln!("Error accepting connection: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,11 @@
|
|||||||
|
use core::time::Duration;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::time::{
|
use std::sync::RwLock;
|
||||||
Duration,
|
|
||||||
SystemTime,
|
|
||||||
UNIX_EPOCH
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
use aws_smithy_types::date_time::{
|
|
||||||
DateTime as AwsDateTime,
|
|
||||||
Format as AwsDateTimeFormat,
|
|
||||||
};
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use tokio::{
|
use tokio::sync::oneshot::Sender;
|
||||||
sync::oneshot::Sender,
|
use tokio::time::sleep;
|
||||||
sync::RwLock,
|
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions, sqlite::SqliteConnectOptions};
|
||||||
time::sleep,
|
|
||||||
};
|
|
||||||
use sqlx::SqlitePool;
|
|
||||||
use sodiumoxide::crypto::{
|
use sodiumoxide::crypto::{
|
||||||
pwhash,
|
pwhash,
|
||||||
pwhash::Salt,
|
pwhash::Salt,
|
||||||
@ -25,47 +14,32 @@ use sodiumoxide::crypto::{
|
|||||||
};
|
};
|
||||||
use tauri::async_runtime as runtime;
|
use tauri::async_runtime as runtime;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use serde::Serializer;
|
|
||||||
|
|
||||||
use crate::{config, config::AppConfig};
|
use crate::{config, config::AppConfig};
|
||||||
use crate::ipc;
|
use crate::ipc;
|
||||||
use crate::clientinfo::Client;
|
use crate::clientinfo::Client;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::server::Server;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(untagged)]
|
||||||
pub struct BaseCredentials {
|
pub enum Credentials {
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
LongLived {
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
secret_access_key: String,
|
secret_access_key: String,
|
||||||
}
|
},
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
#[derive(Clone, Debug, Serialize)]
|
ShortLived {
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub struct SessionCredentials {
|
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
secret_access_key: String,
|
secret_access_key: String,
|
||||||
token: String,
|
token: String,
|
||||||
#[serde(serialize_with = "serialize_expiration")]
|
expiration: String,
|
||||||
expiration: AwsDateTime,
|
},
|
||||||
}
|
|
||||||
|
|
||||||
impl SessionCredentials {
|
|
||||||
fn is_expired(&self) -> bool {
|
|
||||||
let current_ts = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap() // doesn't panic because UNIX_EPOCH won't be later than now()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
let expire_ts = self.expiration.secs();
|
|
||||||
let remaining = expire_ts - (current_ts as i64);
|
|
||||||
remaining < 60
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LockedCredentials {
|
pub struct LockedCredentials {
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
secret_key_enc: Vec<u8>,
|
secret_key_enc: Vec<u8>,
|
||||||
@ -74,12 +48,9 @@ pub struct LockedCredentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Session {
|
pub enum Session {
|
||||||
Unlocked{
|
Unlocked(Credentials),
|
||||||
base: BaseCredentials,
|
|
||||||
session: SessionCredentials,
|
|
||||||
},
|
|
||||||
Locked(LockedCredentials),
|
Locked(LockedCredentials),
|
||||||
Empty,
|
Empty,
|
||||||
}
|
}
|
||||||
@ -92,24 +63,34 @@ pub struct AppState {
|
|||||||
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 bans: RwLock<std::collections::HashSet<Option<Client>>>,
|
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
|
||||||
server: RwLock<Server>,
|
pool: SqlitePool,
|
||||||
pool: sqlx::SqlitePool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(config: AppConfig, session: Session, server: Server, pool: SqlitePool) -> AppState {
|
pub async fn load() -> Result<Self, SetupError> {
|
||||||
AppState {
|
let conn_opts = SqliteConnectOptions::new()
|
||||||
config: RwLock::new(config),
|
.filename(config::get_or_create_db_path())
|
||||||
session: RwLock::new(session),
|
.create_if_missing(true);
|
||||||
|
let pool_opts = SqlitePoolOptions::new();
|
||||||
|
|
||||||
|
let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?;
|
||||||
|
sqlx::migrate!().run(&pool).await?;
|
||||||
|
let creds = Self::load_creds(&pool).await?;
|
||||||
|
let conf = config::load(&pool).await?;
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
config: RwLock::new(conf),
|
||||||
|
session: RwLock::new(creds),
|
||||||
request_count: RwLock::new(0),
|
request_count: RwLock::new(0),
|
||||||
open_requests: RwLock::new(HashMap::new()),
|
open_requests: RwLock::new(HashMap::new()),
|
||||||
bans: RwLock::new(HashSet::new()),
|
bans: RwLock::new(HashSet::new()),
|
||||||
server: RwLock::new(server),
|
|
||||||
pool,
|
pool,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub 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 ORDER BY created_at desc")
|
let res = sqlx::query!("SELECT * FROM credentials ORDER BY created_at desc")
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?;
|
.await?;
|
||||||
@ -134,11 +115,16 @@ impl AppState {
|
|||||||
Ok(Session::Locked(creds))
|
Ok(Session::Locked(creds))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_creds(&self, creds: BaseCredentials, passphrase: &str) -> Result<(), UnlockError> {
|
pub async fn save_creds(&self, creds: Credentials, passphrase: &str) -> Result<(), UnlockError> {
|
||||||
let BaseCredentials {access_key_id, secret_access_key} = creds;
|
let (key_id, secret_key) = match creds {
|
||||||
|
Credentials::LongLived {access_key_id, secret_access_key} => {
|
||||||
|
(access_key_id, secret_access_key)
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
// do this first so that if it fails we don't save bad credentials
|
// do this first so that if it fails we don't save bad credentials
|
||||||
self.new_session(&access_key_id, &secret_access_key).await?;
|
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];
|
||||||
@ -147,14 +133,14 @@ impl AppState {
|
|||||||
// not sure we need both salt AND nonce given that we generate a
|
// not sure we need both salt AND nonce given that we generate a
|
||||||
// fresh salt every time we encrypt, but better safe than sorry
|
// fresh salt every time we encrypt, but better safe than sorry
|
||||||
let nonce = secretbox::gen_nonce();
|
let nonce = secretbox::gen_nonce();
|
||||||
let secret_key_enc = secretbox::seal(secret_access_key.as_bytes(), &nonce, &key);
|
let secret_key_enc = secretbox::seal(secret_key.as_bytes(), &nonce, &key);
|
||||||
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at)
|
"INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at)
|
||||||
VALUES (?, ?, ?, ?, strftime('%s'))"
|
VALUES (?, ?, ?, ?, strftime('%s'))"
|
||||||
)
|
)
|
||||||
.bind(&access_key_id)
|
.bind(&key_id)
|
||||||
.bind(&secret_key_enc)
|
.bind(&secret_key_enc)
|
||||||
.bind(&salt.0[0..])
|
.bind(&salt.0[0..])
|
||||||
.bind(&nonce.0[0..])
|
.bind(&nonce.0[0..])
|
||||||
@ -164,50 +150,30 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
|
pub fn register_request(&self, chan: Sender<ipc::Approval>) -> u64 {
|
||||||
let mut live_config = self.config.write().await;
|
|
||||||
|
|
||||||
if new_config.start_on_login != live_config.start_on_login {
|
|
||||||
config::set_auto_launch(new_config.start_on_login)?;
|
|
||||||
}
|
|
||||||
if new_config.listen_addr != live_config.listen_addr
|
|
||||||
|| new_config.listen_port != live_config.listen_port
|
|
||||||
{
|
|
||||||
let mut sv = self.server.write().await;
|
|
||||||
sv.rebind(new_config.listen_addr, new_config.listen_port).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
new_config.save(&self.pool).await?;
|
|
||||||
*live_config = new_config;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn register_request(&self, chan: Sender<ipc::Approval>) -> u64 {
|
|
||||||
let count = {
|
let count = {
|
||||||
let mut c = self.request_count.write().await;
|
let mut c = self.request_count.write().unwrap();
|
||||||
*c += 1;
|
*c += 1;
|
||||||
c
|
c
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut open_requests = self.open_requests.write().unwrap();
|
||||||
open_requests.insert(*count, chan); // `count` is the request id
|
open_requests.insert(*count, chan); // `count` is the request id
|
||||||
*count
|
*count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unregister_request(&self, id: u64) {
|
pub fn unregister_request(&self, id: u64) {
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut open_requests = self.open_requests.write().unwrap();
|
||||||
open_requests.remove(&id);
|
open_requests.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn req_count(&self) -> usize {
|
pub fn req_count(&self) -> usize {
|
||||||
let open_requests = self.open_requests.read().await;
|
let open_requests = self.open_requests.read().unwrap();
|
||||||
open_requests.len()
|
open_requests.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
pub fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
||||||
self.renew_session_if_expired().await?;
|
let mut open_requests = self.open_requests.write().unwrap();
|
||||||
|
|
||||||
let mut open_requests = self.open_requests.write().await;
|
|
||||||
let chan = open_requests
|
let chan = open_requests
|
||||||
.remove(&response.id)
|
.remove(&response.id)
|
||||||
.ok_or(SendResponseError::NotFound)
|
.ok_or(SendResponseError::NotFound)
|
||||||
@ -217,75 +183,57 @@ impl AppState {
|
|||||||
.map_err(|_e| SendResponseError::Abandoned)
|
.map_err(|_e| SendResponseError::Abandoned)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_ban(&self, client: Option<Client>) {
|
pub fn add_ban(&self, client: Option<Client>, app: tauri::AppHandle) {
|
||||||
let mut bans = self.bans.write().await;
|
let mut bans = self.bans.write().unwrap();
|
||||||
bans.insert(client.clone());
|
bans.insert(client.clone());
|
||||||
|
|
||||||
runtime::spawn(async move {
|
runtime::spawn(async move {
|
||||||
sleep(Duration::from_secs(5)).await;
|
sleep(Duration::from_secs(5)).await;
|
||||||
let app = crate::APP.get().unwrap();
|
|
||||||
let state = app.state::<AppState>();
|
let state = app.state::<AppState>();
|
||||||
let mut bans = state.bans.write().await;
|
let mut bans = state.bans.write().unwrap();
|
||||||
bans.remove(&client);
|
bans.remove(&client);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_banned(&self, client: &Option<Client>) -> bool {
|
pub fn is_banned(&self, client: &Option<Client>) -> bool {
|
||||||
self.bans.read().await.contains(&client)
|
self.bans.read().unwrap().contains(&client)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
|
pub async fn decrypt(&self, passphrase: &str) -> Result<(), UnlockError> {
|
||||||
let mut session = self.session.write().await;
|
let (key_id, secret) = {
|
||||||
let LockedCredentials {
|
// do this all in a block so that we aren't holding a lock across an await
|
||||||
access_key_id,
|
let session = self.session.read().unwrap();
|
||||||
secret_key_enc,
|
let locked = match *session {
|
||||||
salt,
|
|
||||||
nonce
|
|
||||||
} = match *session {
|
|
||||||
Session::Empty => {return Err(UnlockError::NoCredentials);},
|
Session::Empty => {return Err(UnlockError::NoCredentials);},
|
||||||
Session::Unlocked{..} => {return Err(UnlockError::NotLocked);},
|
Session::Unlocked(_) => {return Err(UnlockError::NotLocked);},
|
||||||
Session::Locked(ref c) => c,
|
Session::Locked(ref c) => c,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut key_buf = [0; secretbox::KEYBYTES];
|
let mut key_buf = [0; secretbox::KEYBYTES];
|
||||||
// pretty sure this only fails if we're out of memory
|
// pretty sure this only fails if we're out of memory
|
||||||
pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), salt).unwrap();
|
pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &locked.salt).unwrap();
|
||||||
let decrypted = secretbox::open(secret_key_enc, nonce, &Key(key_buf))
|
let decrypted = secretbox::open(&locked.secret_key_enc, &locked.nonce, &Key(key_buf))
|
||||||
.map_err(|_e| UnlockError::BadPassphrase)?;
|
.map_err(|_e| UnlockError::BadPassphrase)?;
|
||||||
|
|
||||||
let secret_access_key = String::from_utf8(decrypted).map_err(|_e| UnlockError::InvalidUtf8)?;
|
let secret_str = String::from_utf8(decrypted).map_err(|_e| UnlockError::InvalidUtf8)?;
|
||||||
|
(locked.access_key_id.clone(), secret_str)
|
||||||
let session_creds = self.new_session(access_key_id, &secret_access_key).await?;
|
|
||||||
*session = Session::Unlocked {
|
|
||||||
base: BaseCredentials {
|
|
||||||
access_key_id: access_key_id.clone(),
|
|
||||||
secret_access_key,
|
|
||||||
},
|
|
||||||
session: session_creds
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.new_session(&key_id, &secret).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
|
pub fn get_creds_serialized(&self) -> Result<String, GetCredentialsError> {
|
||||||
// let session = self.session.read().await;
|
let session = self.session.read().unwrap();
|
||||||
// match *session {
|
|
||||||
// Session::Unlocked{ref base, ..} => Ok(serde_json::to_string(base).unwrap()),
|
|
||||||
// Session::Locked(_) => Err(GetCredentialsError::Locked),
|
|
||||||
// Session::Empty => Err(GetCredentialsError::Empty),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub async fn serialize_session_creds(&self) -> Result<String, GetCredentialsError> {
|
|
||||||
let session = self.session.read().await;
|
|
||||||
match *session {
|
match *session {
|
||||||
Session::Unlocked{ref session, ..} => Ok(serde_json::to_string(session).unwrap()),
|
Session::Unlocked(ref creds) => Ok(serde_json::to_string(creds).unwrap()),
|
||||||
Session::Locked(_) => Err(GetCredentialsError::Locked),
|
Session::Locked(_) => Err(GetCredentialsError::Locked),
|
||||||
Session::Empty => Err(GetCredentialsError::Empty),
|
Session::Empty => Err(GetCredentialsError::Empty),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn new_session(&self, key_id: &str, secret_key: &str) -> Result<SessionCredentials, GetSessionError> {
|
async fn new_session(&self, key_id: &str, secret_key: &str) -> Result<(), GetSessionError> {
|
||||||
let creds = aws_sdk_sts::Credentials::new(
|
let creds = aws_sdk_sts::Credentials::new(
|
||||||
key_id,
|
key_id,
|
||||||
secret_key,
|
secret_key,
|
||||||
@ -304,58 +252,36 @@ impl AppState {
|
|||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let aws_session = resp.credentials().ok_or(GetSessionError::EmptyResponse)?;
|
let aws_session = resp.credentials().ok_or(GetSessionError::NoCredentials)?;
|
||||||
|
|
||||||
let access_key_id = aws_session.access_key_id()
|
let access_key_id = aws_session.access_key_id()
|
||||||
.ok_or(GetSessionError::EmptyResponse)?
|
.ok_or(GetSessionError::NoCredentials)?
|
||||||
.to_string();
|
.to_string();
|
||||||
let secret_access_key = aws_session.secret_access_key()
|
let secret_access_key = aws_session.secret_access_key()
|
||||||
.ok_or(GetSessionError::EmptyResponse)?
|
.ok_or(GetSessionError::NoCredentials)?
|
||||||
.to_string();
|
.to_string();
|
||||||
let token = aws_session.session_token()
|
let token = aws_session.session_token()
|
||||||
.ok_or(GetSessionError::EmptyResponse)?
|
.ok_or(GetSessionError::NoCredentials)?
|
||||||
.to_string();
|
.to_string();
|
||||||
let expiration = aws_session.expiration()
|
let expiration = aws_session.expiration()
|
||||||
.ok_or(GetSessionError::EmptyResponse)?
|
.ok_or(GetSessionError::NoCredentials)?
|
||||||
.clone();
|
.fmt(aws_smithy_types::date_time::Format::DateTime)
|
||||||
|
.unwrap(); // only fails if the d/t is out of range, which it can't be for this format
|
||||||
|
|
||||||
let session_creds = SessionCredentials {
|
let mut app_session = self.session.write().unwrap();
|
||||||
|
let session_creds = Credentials::ShortLived {
|
||||||
access_key_id,
|
access_key_id,
|
||||||
secret_access_key,
|
secret_access_key,
|
||||||
token,
|
token,
|
||||||
expiration,
|
expiration,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
if cfg!(debug_assertions) {
|
||||||
println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap());
|
println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap());
|
||||||
|
|
||||||
Ok(session_creds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn renew_session_if_expired(&self) -> Result<bool, GetSessionError> {
|
*app_session = Session::Unlocked(session_creds);
|
||||||
match *self.session.write().await {
|
|
||||||
Session::Unlocked{ref base, ref mut session} => {
|
Ok(())
|
||||||
if !session.is_expired() {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
let new_session = self.new_session(
|
|
||||||
&base.access_key_id,
|
|
||||||
&base.secret_access_key
|
|
||||||
).await?;
|
|
||||||
*session = new_session;
|
|
||||||
Ok(true)
|
|
||||||
},
|
|
||||||
Session::Locked(_) => Err(GetSessionError::CredentialsLocked),
|
|
||||||
Session::Empty => Err(GetSessionError::CredentialsEmpty),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn serialize_expiration<S>(exp: &AwsDateTime, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where S: Serializer
|
|
||||||
{
|
|
||||||
// this only fails if the d/t is out of range, which it can't be for this format
|
|
||||||
let time_str = exp.fmt(AwsDateTimeFormat::DateTime).unwrap();
|
|
||||||
serializer.serialize_str(&time_str)
|
|
||||||
}
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"os": {"all": true}
|
"all": true
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@ -48,10 +48,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"csp": {
|
"csp": null
|
||||||
"default-src": ["'self'"],
|
|
||||||
"style-src": ["'self'", "'unsafe-inline'"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": false
|
"active": false
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { emit, listen } from '@tauri-apps/api/event';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
|
||||||
import { appState, acceptRequest } from './lib/state.js';
|
import { appState } from './lib/state.js';
|
||||||
import { views, currentView, navigate } from './lib/routing.js';
|
import { navigate, currentView } from './lib/routing.js';
|
||||||
|
|
||||||
|
|
||||||
$views = import.meta.glob('./views/*.svelte', {eager: true});
|
|
||||||
navigate('Home');
|
|
||||||
|
|
||||||
invoke('get_config').then(config => $appState.config = config);
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
acceptRequest();
|
// can't set this in routing.js directly for some reason
|
||||||
|
if (!$currentView) {
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 14 KiB |
@ -1,11 +1,14 @@
|
|||||||
import { writable, get } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
|
||||||
export let views = writable();
|
const VIEWS = import.meta.glob('../views/*.svelte', {eager: true});
|
||||||
export let currentView = writable();
|
export let currentView = writable();
|
||||||
export let previousView = writable();
|
|
||||||
|
|
||||||
export function navigate(viewName) {
|
export function navigate(viewName) {
|
||||||
let v = get(views)[`./views/${viewName}.svelte`].default;
|
let view = VIEWS[`../views/${viewName}.svelte`].default;
|
||||||
currentView.set(v)
|
currentView.set(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getView(viewName) {
|
||||||
|
return VIEWS[`../views/${viewName}.svelte`].default;
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,9 @@
|
|||||||
import { writable, get } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
import queue from './queue.js';
|
import queue from './queue.js';
|
||||||
import { navigate, currentView, previousView } from './routing.js';
|
|
||||||
|
|
||||||
|
|
||||||
export let appState = writable({
|
export let appState = writable({
|
||||||
currentRequest: null,
|
currentRequest: null,
|
||||||
pendingRequests: queue(),
|
pendingRequests: queue(),
|
||||||
credentialStatus: 'locked',
|
credentialStatus: 'locked',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export async function acceptRequest() {
|
|
||||||
let req = await get(appState).pendingRequests.get();
|
|
||||||
appState.update($appState => {
|
|
||||||
$appState.currentRequest = req;
|
|
||||||
return $appState;
|
|
||||||
});
|
|
||||||
previousView.set(get(currentView));
|
|
||||||
navigate('Approve');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function completeRequest() {
|
|
||||||
appState.update($appState => {
|
|
||||||
$appState.currentRequest = null;
|
|
||||||
return $appState;
|
|
||||||
});
|
|
||||||
currentView.set(get(previousView));
|
|
||||||
previousView.set(null);
|
|
||||||
acceptRequest();
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
.btn-alert-error {
|
|
||||||
@apply bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content
|
|
||||||
}
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
|
|
||||||
let extraClasses = "";
|
let extraClasses;
|
||||||
export {extraClasses as class};
|
export {extraClasses as class};
|
||||||
export let slideDuration = 150;
|
export let slideDuration = 150;
|
||||||
let animationClass = "";
|
let animationClass = "";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { navigate } from '../lib/routing.js';
|
import { navigate, currentView } from '../lib/routing.js';
|
||||||
|
|
||||||
export let target;
|
export let target;
|
||||||
export let hotkey = null;
|
export let hotkey = null;
|
||||||
@ -7,9 +7,6 @@
|
|||||||
export let alt = false;
|
export let alt = false;
|
||||||
export let shift = false;
|
export let shift = false;
|
||||||
|
|
||||||
let classes = "";
|
|
||||||
export {classes as class};
|
|
||||||
|
|
||||||
function click() {
|
function click() {
|
||||||
if (typeof target === 'string') {
|
if (typeof target === 'string') {
|
||||||
navigate(target);
|
navigate(target);
|
||||||
@ -29,7 +26,10 @@
|
|||||||
if (alt && !event.altKey) return;
|
if (alt && !event.altKey) return;
|
||||||
if (shift && !event.shiftKey) return;
|
if (shift && !event.shiftKey) return;
|
||||||
|
|
||||||
if (event.key === hotkey) {
|
if (event.code === hotkey) {
|
||||||
|
click();
|
||||||
|
}
|
||||||
|
else if (hotkey === 'Enter' && event.code === 'NumpadEnter') {
|
||||||
click();
|
click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,6 +38,6 @@
|
|||||||
|
|
||||||
<svelte:window on:keydown={handleHotkey} />
|
<svelte:window on:keydown={handleHotkey} />
|
||||||
|
|
||||||
<a href="/{target}" on:click|preventDefault="{click}" class={classes}>
|
<a href="#" on:click="{click}">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,27 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import Link from './Link.svelte';
|
import Link from './Link.svelte';
|
||||||
import Icon from './Icon.svelte';
|
import Icon from './Icon.svelte';
|
||||||
|
|
||||||
export let position = "sticky";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<nav class="{position} top-0 bg-base-100 w-full flex justify-between items-center p-2">
|
<nav class="fixed top-0 grid grid-cols-2 w-full p-2">
|
||||||
<div>
|
<div>
|
||||||
<Link target="Home">
|
<Link target="Home">
|
||||||
<button class="btn btn-square btn-ghost align-middle">
|
<button class="btn btn-squre btn-ghost align-middle">
|
||||||
<Icon name="home" class="w-8 h-8 stroke-2" />
|
<Icon name="home" class="w-8 h-8 stroke-2" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $$slots.title}
|
<div class="justify-self-end">
|
||||||
<slot name="title"></slot>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Link target="Settings">
|
<Link target="Settings">
|
||||||
<button class="btn btn-square btn-ghost align-middle ">
|
<button class="align-middle btn btn-square btn-ghost">
|
||||||
<Icon name="cog-8-tooth" class="w-8 h-8 stroke-2" />
|
<Icon name="cog-8-tooth" class="w-8 h-8 stroke-2" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -11,43 +11,26 @@
|
|||||||
export let decimal = false;
|
export let decimal = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
$: localValue = value.toString();
|
|
||||||
let lastInputTime = null;
|
|
||||||
function debounce(event) {
|
|
||||||
lastInputTime = Date.now();
|
|
||||||
localValue = localValue.replace(/[^-0-9.]/g, '');
|
|
||||||
|
|
||||||
const eventTime = lastInputTime;
|
|
||||||
const pendingValue = localValue;
|
|
||||||
window.setTimeout(
|
|
||||||
() => {
|
|
||||||
// if no other inputs have occured since then
|
|
||||||
if (eventTime === lastInputTime) {
|
|
||||||
updateValue(pendingValue);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
500
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
function updateValue(newValue) {
|
function validate(event) {
|
||||||
// Don't update the value, but also don't error, if it's empty
|
let v = event.target.value;
|
||||||
// or if it could be the start of a negative or decimal number
|
|
||||||
if (newValue.match(/^$|^-$|^\.$/) !== null) {
|
if (v === '') {
|
||||||
error = null;
|
error = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const num = parseFloat(newValue);
|
let num = parseFloat(v);
|
||||||
if (num % 1 !== 0 && !decimal) {
|
if (Number.isNaN(num)) {
|
||||||
|
error = `"${v}" is not a number`;
|
||||||
|
}
|
||||||
|
else if (num % 1 !== 0 && !decimal) {
|
||||||
error = `${num} is not a whole number`;
|
error = `${num} is not a whole number`;
|
||||||
}
|
}
|
||||||
else if (min !== null && num < min) {
|
else if (min && num < min) {
|
||||||
error = `Too low (minimum ${min})`;
|
error = `Too low (minimum ${min})`;
|
||||||
}
|
}
|
||||||
else if (max !== null && num > max) {
|
else if (max && num > max) {
|
||||||
error = `Too large (maximum ${max})`
|
error = `Too large (maximum ${max})`
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -59,19 +42,18 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Setting {title}>
|
<Setting {title} {error}>
|
||||||
<div slot="input">
|
<div slot="input">
|
||||||
{#if unit}
|
{#if unit}
|
||||||
<span class="mr-2">{unit}:</span>
|
<span class="mr-2">{unit}:</span>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="tooltip tooltip-error" class:tooltip-open={error !== null} data-tip="{error}">
|
<div class="tooltip tooltip-error" class:tooltip-open={error !== null} data-tip={error}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-sm input-bordered text-right"
|
class="input input-sm input-bordered text-right max-w-[4rem]"
|
||||||
size="{Math.max(5, localValue.length)}"
|
|
||||||
class:input-error={error}
|
class:input-error={error}
|
||||||
bind:value={localValue}
|
value={value}
|
||||||
on:input="{debounce}"
|
on:input="{validate}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import ErrorAlert from '../ErrorAlert.svelte';
|
import ErrorAlert from '../ErrorAlert.svelte';
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
|
export let error = null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -12,8 +13,6 @@
|
|||||||
<slot name="input"></slot>
|
<slot name="input"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $$slots.description}
|
<p class="mt-3">
|
||||||
<p class="mt-3">
|
|
||||||
<slot name="description"></slot>
|
<slot name="description"></slot>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Setting {title}>
|
<Setting title="Start minimized">
|
||||||
<input
|
<input
|
||||||
slot="input"
|
slot="input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -1,35 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
|
||||||
import { navigate } from '../lib/routing.js';
|
import { navigate } from '../lib/routing.js';
|
||||||
import { appState, completeRequest } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
|
import Icon from '../ui/Icon.svelte';
|
||||||
|
|
||||||
|
|
||||||
// Send response to backend, display error if applicable
|
|
||||||
let error, alert;
|
|
||||||
async function respond() {
|
|
||||||
let {id, approval} = $appState.currentRequest;
|
|
||||||
try {
|
|
||||||
await invoke('respond', {response: {id, approval}});
|
|
||||||
navigate('ShowResponse');
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if (error) {
|
|
||||||
alert.shake();
|
|
||||||
}
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Approval has one of several outcomes depending on current credential state
|
|
||||||
async function approve() {
|
async function approve() {
|
||||||
$appState.currentRequest.approval = 'Approved';
|
|
||||||
let status = await invoke('get_session_status');
|
let status = await invoke('get_session_status');
|
||||||
if (status === 'unlocked') {
|
if (status === 'unlocked') {
|
||||||
await respond();
|
navigate('ShowApproved');
|
||||||
}
|
}
|
||||||
else if (status === 'locked') {
|
else if (status === 'locked') {
|
||||||
navigate('Unlock');
|
navigate('Unlock');
|
||||||
@ -39,69 +20,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Denial has only one
|
var appName = null;
|
||||||
async function deny() {
|
|
||||||
$appState.currentRequest.approval = 'Denied';
|
|
||||||
await respond();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract executable name from full path
|
|
||||||
let appName = null;
|
|
||||||
if ($appState.currentRequest.clients.length === 1) {
|
if ($appState.currentRequest.clients.length === 1) {
|
||||||
let path = $appState.currentRequest.clients[0].exe;
|
let path = $appState.currentRequest.clients[0].exe;
|
||||||
let m = path.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
let m = path.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
||||||
appName = m[1] || m[2];
|
appName = m[1] || m[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Executable paths can be long, so ensure they only break on \ or /
|
|
||||||
function breakPath(client) {
|
|
||||||
return client.exe.replace(/(\\|\/)/g, '$1<wbr>');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the request has already been approved/denied, send response immediately
|
|
||||||
onMount(async () => {
|
|
||||||
if ($appState.currentRequest.approval) {
|
|
||||||
await respond();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<!-- Don't render at all if we're just going to immediately proceed to the next screen -->
|
<div class="flex flex-col space-y-4 p-4 m-auto max-w-max h-screen justify-center">
|
||||||
{#if !$appState.currentRequest.approval}
|
<!-- <div class="p-4 rounded-box border-2 border-neutral-content"> -->
|
||||||
<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}
|
|
||||||
<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={respond}>Retry</button>
|
|
||||||
</svelte:fragment>
|
|
||||||
</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="space-y-1 mb-4">
|
<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>
|
<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">
|
|
||||||
{#each $appState.currentRequest.clients as client}
|
{#each $appState.currentRequest.clients as client}
|
||||||
<div class="text-right">Path:</div>
|
<p>Path: {client ? client.exe : 'Unknown'}</p>
|
||||||
<code class="">{@html client ? breakPath(client) : 'Unknown'}</code>
|
<p>PID: {client ? client.pid : 'Unknown'}</p>
|
||||||
<div class="text-right">PID:</div>
|
|
||||||
<code>{client ? client.pid : 'Unknown'}</code>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between">
|
<div class="grid grid-cols-2">
|
||||||
<Link target={deny} hotkey="Escape">
|
<Link target="ShowDenied" hotkey="Escape">
|
||||||
<button class="btn btn-error justify-self-start">
|
<button class="btn btn-error justify-self-start">
|
||||||
Deny
|
Deny
|
||||||
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
|
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link target={approve} hotkey="Enter" shift="{true}">
|
<Link target="{approve}" hotkey="Enter" shift="{true}">
|
||||||
<button class="btn btn-success justify-self-end">
|
<button class="btn btn-success justify-self-end">
|
||||||
Approve
|
Approve
|
||||||
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
|
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
|
||||||
@ -110,5 +56,4 @@
|
|||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
@ -11,25 +11,16 @@
|
|||||||
|
|
||||||
let errorMsg = null;
|
let errorMsg = null;
|
||||||
let alert;
|
let alert;
|
||||||
let AccessKeyId, SecretAccessKey, passphrase, confirmPassphrase
|
let AccessKeyId, SecretAccessKey, passphrase
|
||||||
|
|
||||||
function confirm() {
|
|
||||||
if (passphrase !== confirmPassphrase) {
|
|
||||||
errorMsg = 'Passphrases do not match.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (passphrase !== confirmPassphrase) {
|
console.log('Saving credentials.');
|
||||||
alert.shake();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let credentials = {AccessKeyId, SecretAccessKey};
|
let credentials = {AccessKeyId, SecretAccessKey};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invoke('save_credentials', {credentials, passphrase});
|
await invoke('save_credentials', {credentials, passphrase});
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('ShowApproved');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
navigate('Home');
|
navigate('Home');
|
||||||
@ -63,7 +54,6 @@
|
|||||||
<input type="text" placeholder="AWS Access Key ID" bind:value="{AccessKeyId}" class="input input-bordered" />
|
<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="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="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} />
|
|
||||||
|
|
||||||
<input type="submit" class="btn btn-primary" />
|
<input type="submit" class="btn btn-primary" />
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target="Home" hotkey="Escape">
|
||||||
|
@ -8,41 +8,34 @@
|
|||||||
import Icon from '../ui/Icon.svelte';
|
import Icon from '../ui/Icon.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
|
|
||||||
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
// onMount(async () => {
|
// will block until a request comes in
|
||||||
// // will block until a request comes in
|
let req = await $appState.pendingRequests.get();
|
||||||
// let req = await $appState.pendingRequests.get();
|
$appState.currentRequest = req;
|
||||||
// $appState.currentRequest = req;
|
navigate('Approve');
|
||||||
// navigate('Approve');
|
});
|
||||||
// });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Nav position="fixed">
|
<Nav />
|
||||||
<h2 slot="title" class="text-3xl font-bold">Creddy</h2>
|
|
||||||
</Nav>
|
|
||||||
|
|
||||||
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
<div class="flex flex-col h-screen justify-center items-center space-y-4">
|
||||||
{#await invoke('get_session_status') then status}
|
{#await invoke('get_session_status') then status}
|
||||||
{#if status === 'locked'}
|
{#if status === 'locked'}
|
||||||
|
<img src="/static/padlock-closed.svg" alt="A locked padlock" class="w-32" />
|
||||||
{@html vaultDoorSvg}
|
|
||||||
<h2 class="text-2xl font-bold">Creddy is locked</h2>
|
<h2 class="text-2xl font-bold">Creddy is locked</h2>
|
||||||
<Link target="Unlock" hotkey="Enter" class="w-64">
|
<Link target="Unlock">
|
||||||
<button class="btn btn-primary w-full">Unlock</button>
|
<button class="btn btn-primary">Unlock</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{:else if status === 'unlocked'}
|
{:else if status === 'unlocked'}
|
||||||
{@html vaultDoorSvg}
|
<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>
|
||||||
|
|
||||||
{:else if status === 'empty'}
|
{:else if status === 'empty'}
|
||||||
{@html vaultDoorSvg}
|
<Link target="EnterCredentials">
|
||||||
<h2 class="text-2xl font-bold">No credentials found</h2>
|
<button class="btn btn-primary">Enter Credentials</button>
|
||||||
<Link target="EnterCredentials" hotkey="Enter" class="w-64">
|
|
||||||
<button class="btn btn-primary w-full">Enter Credentials</button>
|
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
|
@ -1,46 +1,24 @@
|
|||||||
<script>
|
<script>
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import { type } from '@tauri-apps/api/os';
|
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
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 ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
|
// import Setting from '../ui/settings/Setting.svelte';
|
||||||
import { Setting, ToggleSetting, NumericSetting } from '../ui/settings';
|
import { Setting, ToggleSetting, NumericSetting } from '../ui/settings';
|
||||||
|
|
||||||
import { fly } from 'svelte/transition';
|
|
||||||
import { backInOut } from 'svelte/easing';
|
|
||||||
|
|
||||||
|
|
||||||
let error = null;
|
|
||||||
async function save() {
|
async function save() {
|
||||||
try {
|
|
||||||
await invoke('save_config', {config: $appState.config});
|
await invoke('save_config', {config: $appState.config});
|
||||||
}
|
}
|
||||||
catch (e) {
|
|
||||||
error = e;
|
|
||||||
$appState.config = await invoke('get_config');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let osType = '';
|
|
||||||
type().then(t => osType = t);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Nav>
|
<Nav />
|
||||||
<h2 slot="title" class="text-2xl font-bold">Settings</h2>
|
|
||||||
</Nav>
|
|
||||||
|
|
||||||
{#await invoke('get_config') then config}
|
{#await invoke('get_config') then config}
|
||||||
<div class="max-w-md mx-auto mt-1.5 p-4">
|
<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>
|
||||||
|
|
||||||
<ToggleSetting title="Start on login" bind:value={$appState.config.start_on_login} on:update={save}>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Start Creddy when you log in to your computer.
|
|
||||||
</svelte:fragment>
|
|
||||||
</ToggleSetting>
|
|
||||||
|
|
||||||
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
|
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
@ -56,18 +34,6 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</NumericSetting>
|
</NumericSetting>
|
||||||
|
|
||||||
<NumericSetting
|
|
||||||
title="Listen port"
|
|
||||||
bind:value={$appState.config.listen_port}
|
|
||||||
min={osType === 'Windows_NT' ? 1 : 0}
|
|
||||||
on:update={save}
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Listen for credentials requests on this port.
|
|
||||||
(Should be used with <code>$AWS_CONTAINER_CREDENTIALS_FULL_URI</code>)
|
|
||||||
</svelte:fragment>
|
|
||||||
</NumericSetting>
|
|
||||||
|
|
||||||
<Setting title="Update credentials">
|
<Setting title="Update credentials">
|
||||||
<Link slot="input" target="EnterCredentials">
|
<Link slot="input" target="EnterCredentials">
|
||||||
<button class="btn btn-sm btn-primary">Update</button>
|
<button class="btn btn-sm btn-primary">Update</button>
|
||||||
@ -78,17 +44,3 @@
|
|||||||
</Setting>
|
</Setting>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
|
||||||
<div class="alert alert-error no-animation">
|
|
||||||
<div>
|
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-sm btn-alert-error" on:click={() => error = null}>Ok</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
75
src/views/ShowApproved.svelte
Normal file
75
src/views/ShowApproved.svelte
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { draw, fade } from 'svelte/transition';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
|
||||||
|
import { appState } from '../lib/state.js';
|
||||||
|
import { navigate } from '../lib/routing.js';
|
||||||
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
|
import Icon from '../ui/Icon.svelte';
|
||||||
|
import Link from '../ui/Link.svelte';
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
async function respond() {
|
||||||
|
let response = {
|
||||||
|
id: $appState.currentRequest.id,
|
||||||
|
approval: 'Approved',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('respond', {response});
|
||||||
|
success = true;
|
||||||
|
$appState.currentRequest = null;
|
||||||
|
window.setTimeout(() => navigate('Home'), 1000);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(respond);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg">
|
||||||
|
<ErrorAlert>
|
||||||
|
{error}
|
||||||
|
<svelte:fragment slot="buttons">
|
||||||
|
<Link target="Home">
|
||||||
|
<button class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content">
|
||||||
|
Ok
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ErrorAlert>
|
||||||
|
</div>
|
||||||
|
{:else if success}
|
||||||
|
<div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
|
||||||
|
<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 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
|
||||||
|
<div in:fade="{{delay: 200, duration: 300}}" class="text-2xl font-bold">Approved!</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{#if error}
|
||||||
|
<div class="text-red-400">{error}</div>
|
||||||
|
{:else}
|
||||||
|
<h1 class="text-4xl text-gray-300">Approved!</h1>
|
||||||
|
{/if}
|
||||||
|
-->
|
56
src/views/ShowDenied.svelte
Normal file
56
src/views/ShowDenied.svelte
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { draw, fade } from 'svelte/transition';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
|
||||||
|
import { appState } from '../lib/state.js';
|
||||||
|
import { navigate } from '../lib/routing.js';
|
||||||
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
|
import Icon from '../ui/Icon.svelte';
|
||||||
|
import Link from '../ui/Link.svelte';
|
||||||
|
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
async function respond() {
|
||||||
|
let response = {
|
||||||
|
id: $appState.currentRequest.id,
|
||||||
|
approval: 'Denied',
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('respond', {response});
|
||||||
|
$appState.currentRequest = null;
|
||||||
|
window.setTimeout(() => navigate('Home'), 1000);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(respond);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg">
|
||||||
|
<ErrorAlert>
|
||||||
|
{error}
|
||||||
|
<svelte:fragment slot="buttons">
|
||||||
|
<Link target="Home">
|
||||||
|
<button class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content" on:click="{() => navigate('Home')}">
|
||||||
|
Ok
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ErrorAlert>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center justify-center h-screen max-w-max m-auto">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div in:fade="{{delay: 200, duration: 300}}" class="text-2xl font-bold">Denied!</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -1,38 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { draw, fade } from 'svelte/transition';
|
|
||||||
|
|
||||||
import { appState, completeRequest } 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;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
window.setTimeout(
|
|
||||||
completeRequest,
|
|
||||||
// Extra 50ms so the window can finish disappearing before the redraw
|
|
||||||
Math.min(5000, $appState.config.rehide_ms + 50),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
|
|
||||||
{#if $appState.currentRequest.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.approval}!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
import { navigate } from '../lib/routing.js';
|
import { navigate } from '../lib/routing.js';
|
||||||
@ -12,19 +11,12 @@
|
|||||||
let errorMsg = null;
|
let errorMsg = null;
|
||||||
let alert;
|
let alert;
|
||||||
let passphrase = '';
|
let passphrase = '';
|
||||||
let loadTime = 0;
|
|
||||||
async function unlock() {
|
async function unlock() {
|
||||||
// The hotkey for navigating here from homepage is Enter, which also
|
|
||||||
// happens to trigger the form submit event
|
|
||||||
if (Date.now() - loadTime < 10) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let r = await invoke('unlock', {passphrase});
|
let r = await invoke('unlock', {passphrase});
|
||||||
$appState.credentialStatus = 'unlocked';
|
$appState.credentialStatus = 'unlocked';
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('ShowApproved');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
navigate('Home');
|
navigate('Home');
|
||||||
@ -45,10 +37,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
loadTime = Date.now();
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +47,6 @@
|
|||||||
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
|
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
|
||||||
<input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
|
<input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
|
||||||
|
|
||||||
<input type="submit" class="btn btn-primary" />
|
<input type="submit" class="btn btn-primary" />
|
||||||
|
430
static/padlock-closed.svg
Normal file
430
static/padlock-closed.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 34 KiB |
467
static/padlock-open.svg
Normal file
467
static/padlock-open.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 46 KiB |
Loading…
x
Reference in New Issue
Block a user