Compare commits

..

5 Commits

45 changed files with 3593 additions and 5131 deletions

View File

@ -11,8 +11,7 @@
* Logging * Logging
* Icon * Icon
* Auto-updates * Auto-updates
* ~~SSH key handling~~ * SSH key handling
* ~~Docker credential helper~~
* Encrypted sync server * Encrypted sync server
## Maybe ## Maybe

92
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "creddy", "name": "creddy",
"version": "0.6.4", "version": "0.4.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "creddy", "name": "creddy",
"version": "0.6.4", "version": "0.4.9",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.0.0-beta.13", "@tauri-apps/api": "^2.0.0-beta.13",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.5", "@tauri-apps/plugin-dialog": "^2.0.0-beta.5",
@ -15,7 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tauri-apps/cli": "^2.2.1", "@tauri-apps/cli": "^2.0.0-beta.20",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"postcss": "^8.4.16", "postcss": "^8.4.16",
"svelte": "^3.49.0", "svelte": "^3.49.0",
@ -213,9 +213,9 @@
} }
}, },
"node_modules/@tauri-apps/cli": { "node_modules/@tauri-apps/cli": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-beta.20.tgz",
"integrity": "sha512-oLWX/2tW0v8cBaShI9/bt5RsquCLK7ZCwhPXXnf55oil8/GrNtLzW9/67iyydcnxiYYU5jYMpo3uXptknOSdpA==", "integrity": "sha512-707q9uIc2oNrYHd2dtMvxTrpZXVpart5EIktnRymNOpphkLlB6WUBjHD+ga45WqTU6cNGKbYvkKqTNfshNul9Q==",
"dev": true, "dev": true,
"bin": { "bin": {
"tauri": "tauri.js" "tauri": "tauri.js"
@ -228,22 +228,22 @@
"url": "https://opencollective.com/tauri" "url": "https://opencollective.com/tauri"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.2.1", "@tauri-apps/cli-darwin-arm64": "2.0.0-beta.20",
"@tauri-apps/cli-darwin-x64": "2.2.1", "@tauri-apps/cli-darwin-x64": "2.0.0-beta.20",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.2.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-beta.20",
"@tauri-apps/cli-linux-arm64-gnu": "2.2.1", "@tauri-apps/cli-linux-arm64-gnu": "2.0.0-beta.20",
"@tauri-apps/cli-linux-arm64-musl": "2.2.1", "@tauri-apps/cli-linux-arm64-musl": "2.0.0-beta.20",
"@tauri-apps/cli-linux-x64-gnu": "2.2.1", "@tauri-apps/cli-linux-x64-gnu": "2.0.0-beta.20",
"@tauri-apps/cli-linux-x64-musl": "2.2.1", "@tauri-apps/cli-linux-x64-musl": "2.0.0-beta.20",
"@tauri-apps/cli-win32-arm64-msvc": "2.2.1", "@tauri-apps/cli-win32-arm64-msvc": "2.0.0-beta.20",
"@tauri-apps/cli-win32-ia32-msvc": "2.2.1", "@tauri-apps/cli-win32-ia32-msvc": "2.0.0-beta.20",
"@tauri-apps/cli-win32-x64-msvc": "2.2.1" "@tauri-apps/cli-win32-x64-msvc": "2.0.0-beta.20"
} }
}, },
"node_modules/@tauri-apps/cli-darwin-arm64": { "node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.20.tgz",
"integrity": "sha512-658OPWObcEA7x/Pe/fAXfyJtC5SdcpD2Q9ZSVKoLBovPzfU6Ug2mCaQmH1L5iA7Zb7a26ctzkaz3Sh3dMeGcJw==", "integrity": "sha512-oCJOCib7GuYkwkBXx+ekamR8NZZU+2i3MLP+DHpDxK5gS2uhCE+CBkamJkNt6y1x6xdVnwyqZOm5RvN4SRtyIA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -257,9 +257,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-darwin-x64": { "node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.20.tgz",
"integrity": "sha512-3g11km4caJa6StvETI5GIynniNC/e9AWpUy+lWQRfQBdelRrEGoEDw949SihxqKHAoP2E9cm7z5DUsiRiT/Yaw==", "integrity": "sha512-lC5QSnRExedYN4Ds6ZlSvC2PxP8qfIYBJQ5ktf+PJI5gQALdNeVtd6YnTG1ODCEklfLq9WKkGwp7JdALTU5wDA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -273,9 +273,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.20.tgz",
"integrity": "sha512-Ldbw3Y56TAfpsGRuWJnkdl0TV0NHhtP3bGyjh2lJACofkHMCOtsLHOx4/HP2hFnn7DcSLWHUayyPlj2rAikKkA==", "integrity": "sha512-nZCeBMHHye5DLOJV5k2w658hnCS+LYaOZ8y/G9l3ei+g0L/HBjlSy6r4simsAT5TG8+l3oCZzLBngfTMdDS/YA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -289,9 +289,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm64-gnu": { "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.20.tgz",
"integrity": "sha512-ay3NwilDR95RyvK/AIdivuULcbpGgrUISNLDOfTKEvKMMnRWkMV4gzY3hifQ8H7CDonGhqMl2PjP+WvDQpXUig==", "integrity": "sha512-B79ISVLPVBgwnCchVqwTKU+vxnFYqxKomcR4rmsvxfs0NVtT5QuNzE1k4NUQnw3966yjwhYR3mnHsSJQSB4Eyw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -305,9 +305,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm64-musl": { "node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.20.tgz",
"integrity": "sha512-d2zK4Qb9DZlNjNB8Fda0yxOlg6sk6GZGhO5dVnie5VYJMt4lDct2LZljg4boUb5t1pk6sfAPB9356G7R8l4qCQ==", "integrity": "sha512-ojIkv/1uZHhcrgfIN8xgn4BBeo/Xg+bnV0wer6lD78zyxkUMWeEZ+u3mae1ejCJNhhaZOxNaUQ67MvDOiGyr5Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -321,9 +321,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-x64-gnu": { "node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.20.tgz",
"integrity": "sha512-P0Zm3nmRbBS/KIxSrzul2ieZEwtTdU4bjsB9pOIk+oPF15HXnrLLbVBeMofNjXOWsIxTJw2tIt/XPD8Jt9jSEg==", "integrity": "sha512-xBy1FNbHKlc7T6pOmFQQPECxJaI5A9QWX7Kb9N64cNVusoOGlvc3xHYkXMS4PTr7xXOT0yiE1Ww2OwDRJ3lYsg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -337,9 +337,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-x64-musl": { "node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.20.tgz",
"integrity": "sha512-AwYuKTpPGdR0BJMDdJsjGm8vfVDBpXYRDJ+1B/FlIMTikAx4A/wSODxphjf6Ls9uOC5F3To0XlfqskBkTq0WKw==", "integrity": "sha512-+O6zq5jmtUxA1FUAAwF2ywPysy4NRo2Y6G+ESZDkY9XosRwdt5OUjqAsYktZA3AxDMZVei8r9buwTqUwi9ny/g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -353,9 +353,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-arm64-msvc": { "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.20.tgz",
"integrity": "sha512-t1Pv+Og5O+Cp0uYHFzSWEl+hssr1bKJjgWg05ElTpwYMb4xKA5bh1BTGN5orGqKs0e2+D+EPsOqVfM8KuUWR4Q==", "integrity": "sha512-RswgMbWyOQcv53CHvIuiuhAh4kKDqaGyZfWD4VlxqX/XhkoF5gsNgr0MxzrY7pmoL+89oVI+fiGVJz4nOQE5vA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -369,9 +369,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-ia32-msvc": { "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.20.tgz",
"integrity": "sha512-erY+Spho6hBJgNzHKbA3JFxMztlHAikCiF/OYhk9fy6MbU5KpYHPrAC+Jhj2tcDy/xevWw/6KVNvLmk9PhLcXQ==", "integrity": "sha512-5lgWmDVXhX3SBGbiv5SduM1yajiRnUEJClWhSdRrEEJeXdsxpCsBEhxYnUnDCEzPKxLLn5fdBv3VrVctJ03csQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -385,9 +385,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-x64-msvc": { "node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.2.1", "version": "2.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.20.tgz",
"integrity": "sha512-GIdUtdje1CvCn0/Sh3VwPWaFKmD1C0edJUMueGwkRFHmF6HfatXPVhW5FySP+EEO2+rVym1qJkODstJrunraWA==", "integrity": "sha512-SuSiiVQTQPSzWlsxQp/NMzWbzDS9TdVDOw7CCfgiG5wnT2GsxzrcIAVN6i7ILsVFLxrjr0bIgPldSJcdcH84Yw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],

View File

@ -1,6 +1,6 @@
{ {
"name": "creddy", "name": "creddy",
"version": "0.6.5", "version": "0.5.4",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@ -9,7 +9,7 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tauri-apps/cli": "^2.2.1", "@tauri-apps/cli": "^2.0.0-beta.20",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"postcss": "^8.4.16", "postcss": "^8.4.16",
"svelte": "^3.49.0", "svelte": "^3.49.0",

1515
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "creddy" name = "creddy"
version = "0.6.5" version = "0.5.4"
description = "A friendly AWS credentials manager" description = "A friendly AWS credentials manager"
authors = ["Joseph Montanaro"] authors = ["Joseph Montanaro"]
license = "" license = ""
@ -22,16 +22,15 @@ dirs = "5.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tokio = { version = ">=1.19", features = ["full"] } tokio = { version = ">=1.19", features = ["full"] }
windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.4", features = [] } tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies] [dependencies]
creddy_cli = { path = "./creddy_cli" } creddy_cli = { path = "./creddy_cli" }
tauri = { version = "2.2.0", features = ["tray-icon", "test"] } tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
sodiumoxide = "0.2.7" sodiumoxide = "0.2.7"
sysinfo = "0.26.8" sysinfo = "0.26.8"
aws-config = "1.5.3" aws-config = "1.5.3"
@ -43,15 +42,17 @@ 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.5.0" auto-launch = "0.4.0"
is-terminal = "0.4.7" is-terminal = "0.4.7"
argon2 = { version = "0.5.0", features = ["std"] } argon2 = { version = "0.5.0", features = ["std"] }
chacha20poly1305 = { version = "0.10.1", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] }
which = "4.4.0" which = "4.4.0"
windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] }
time = "0.3.31" time = "0.3.31"
tauri-plugin-global-shortcut = "2.2.0" tauri-plugin-single-instance = "2.0.0-beta.9"
tauri-plugin-os = "2.2.0" tauri-plugin-global-shortcut = "2.0.0-beta.6"
tauri-plugin-dialog = "2.2.0" tauri-plugin-os = "2.0.0-beta.6"
tauri-plugin-dialog = "2.0.0-beta.9"
rfd = "0.13.0" rfd = "0.13.0"
ssh-agent-lib = "0.4.0" ssh-agent-lib = "0.4.0"
ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] } ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] }
@ -63,14 +64,11 @@ sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] }
tokio = { workspace = true } tokio = { workspace = true }
tokio-util = { version = "0.7.11", features = ["codec"] } tokio-util = { version = "0.7.11", features = ["codec"] }
futures = "0.3.30" futures = "0.3.30"
# openssl = { version = "0.10.64", features = ["vendored"] } openssl = "0.10.64"
rsa = "0.9.6" rsa = "0.9.6"
sha2 = "0.10.8" sha2 = "0.10.8"
ssh-encoding = "0.2.0" ssh-encoding = "0.2.0"
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }
[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

View File

@ -6,13 +6,13 @@
"main" "main"
], ],
"permissions": [ "permissions": [
"core:path:default", "path:default",
"core:event:default", "event:default",
"core:window:default", "window:default",
"core:app:default", "app:default",
"core:resources:default", "resources:default",
"core:menu:default", "menu:default",
"core:tray:default", "tray:default",
"os:allow-os-type", "os:allow-os-type",
"dialog:allow-open" "dialog:allow-open"
] ]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "creddy_cli" name = "creddy_cli"
version = "0.6.5" version = "0.5.4"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -10,6 +10,3 @@ dirs = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }

View File

@ -13,7 +13,11 @@ use super::{
pub fn docker_store(global_args: GlobalArgs) -> anyhow::Result<()> { pub fn docker_store(global_args: GlobalArgs) -> anyhow::Result<()> {
let input: DockerCredential = serde_json::from_reader(io::stdin())?; let input: DockerCredential = serde_json::from_reader(io::stdin())?;
let req = CliRequest::StoreDockerCredential(input); let req = CliRequest::SaveCredential {
name: input.username.clone(),
is_default: false, // is_default doesn't really mean anything for Docker credentials
credential: CliCredential::Docker(input),
};
match super::make_request(global_args.server_addr, &req)?? { match super::make_request(global_args.server_addr, &req)?? {
CliResponse::Empty => Ok(()), CliResponse::Empty => Ok(()),
@ -29,34 +33,11 @@ pub fn docker_get(global_args: GlobalArgs) -> anyhow::Result<()> {
server_url: server_url.trim().to_owned() server_url: server_url.trim().to_owned()
}; };
let server_resp = super::make_request(global_args.server_addr, &req)?; match super::make_request(global_args.server_addr, &req)?? {
match server_resp { CliResponse::Credential(CliCredential::Docker(d)) => {
Ok(CliResponse::Credential(CliCredential::Docker(d))) => {
println!("{}", serde_json::to_string(&d)?); println!("{}", serde_json::to_string(&d)?);
}, },
Err(e) if e.code == "NoCredentials" => { r => bail!("Unexpected response from server: {r}"),
// To indicate credentials are not found, a credential helper *must* print
// this message to stdout, then exit 1. Any other message/status will cause
// some builds to fail. This is, of course, not documented anywhere.
println!("credentials not found in native keychain");
std::process::exit(1);
},
Err(e) => Err(e)?,
Ok(r) => bail!("Unexpected response from server: {r}"),
} }
Ok(()) Ok(())
} }
pub fn docker_erase(global_args: GlobalArgs) -> anyhow::Result<()> {
let mut server_url = String::new();
io::stdin().read_to_string(&mut server_url)?;
let req = CliRequest::EraseDockerCredential {
server_url: server_url.trim().to_owned()
};
match super::make_request(global_args.server_addr, &req)?? {
CliResponse::Empty => Ok(()),
r => bail!("Unexpected response from server: {r}"),
}
}

View File

@ -2,6 +2,8 @@ use std::path::PathBuf;
use std::process::Command as ChildCommand; use std::process::Command as ChildCommand;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
#[cfg(windows)]
use std::time::Duration;
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use clap::{ use clap::{
@ -63,7 +65,7 @@ pub struct GlobalArgs {
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum Action { pub enum Action {
/// Launch Creddy /// Launch Creddy
Run(RunArgs), Run,
/// Request credentials from Creddy and output to stdout /// Request credentials from Creddy and output to stdout
Get(GetArgs), Get(GetArgs),
/// Inject credentials into the environment of another command /// Inject credentials into the environment of another command
@ -76,14 +78,6 @@ pub enum Action {
} }
#[derive(Debug, Args)]
pub struct RunArgs {
/// Minimize to system tray on launch
#[arg(long, default_value_t = false)]
pub minimized: bool,
}
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct GetArgs { pub struct GetArgs {
/// If unspecified, use default credentials /// If unspecified, use default credentials
@ -108,7 +102,7 @@ pub struct ExecArgs {
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct InvokeArgs { pub struct InvokeArgs {
#[arg(value_name = "ACTION", value_enum)] #[arg(value_name = "ACTION", value_enum)]
pub shortcut_action: ShortcutAction, shortcut_action: ShortcutAction,
} }
@ -182,17 +176,24 @@ pub fn exec(args: ExecArgs, global: GlobalArgs) -> anyhow::Result<()> {
#[cfg(windows)] #[cfg(windows)]
{ {
let mut child = cmd.spawn() let mut child = match cmd.spawn() {
.with_context(|| format!("Failed to execute command: {}", args.command.join(" ")))?; Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let name: OsString = cmd_name.into();
return Err(ExecError::NotFound(name).into());
}
Err(e) => return Err(ExecError::ExecutionFailed(e).into()),
};
let status = child.wait() let status = child.wait()
.with_context(|| format!("Failed to execute command: {}", args.command.join(" ")))?; .map_err(|e| ExecError::ExecutionFailed(e))?;
std::process::exit(status.code().unwrap_or(1)); std::process::exit(status.code().unwrap_or(1));
}; };
} }
pub fn invoke_shortcut(args: InvokeArgs, global: GlobalArgs) -> anyhow::Result<()> { pub fn invoke_shortcut(args: InvokeArgs, global: GlobalArgs) -> anyhow::Result<()> {
let req = CliRequest::InvokeShortcut{action: args.shortcut_action}; let req = CliRequest::InvokeShortcut(args.shortcut_action);
match make_request(global.server_addr, &req)?? { match make_request(global.server_addr, &req)?? {
CliResponse::Empty => Ok(()), CliResponse::Empty => Ok(()),
r => bail!("Unexpected response from server: {r}"), r => bail!("Unexpected response from server: {r}"),
@ -204,7 +205,7 @@ pub fn docker_credential_helper(cmd: DockerCmd, global_args: GlobalArgs) -> anyh
match cmd { match cmd {
DockerCmd::Get => docker::docker_get(global_args), DockerCmd::Get => docker::docker_get(global_args),
DockerCmd::Store => docker::docker_store(global_args), DockerCmd::Store => docker::docker_store(global_args),
DockerCmd::Erase => docker::docker_erase(global_args), DockerCmd::Erase => todo!(),
} }
} }

View File

@ -1,26 +1,19 @@
mod cli; mod cli;
pub use cli::{ pub use cli::{
Action,
Cli, Cli,
docker_credential_helper, Action,
exec, exec,
get, get,
GlobalArgs,
RunArgs,
invoke_shortcut, invoke_shortcut,
docker_credential_helper,
}; };
pub use platform::{connect, server_addr}; pub(crate) use platform::connect;
pub use platform::server_addr;
pub mod proto; pub mod proto;
pub fn show_window(global_args: GlobalArgs) -> anyhow::Result<()> {
let invoke = cli::InvokeArgs { shortcut_action: proto::ShortcutAction::ShowWindow };
cli::invoke_shortcut(invoke, global_args)
}
#[cfg(unix)] #[cfg(unix)]
mod platform { mod platform {
use std::path::PathBuf; use std::path::PathBuf;
@ -34,12 +27,7 @@ mod platform {
pub fn server_addr(sock_name: &str) -> PathBuf { pub fn server_addr(sock_name: &str) -> PathBuf {
let mut path = dirs::runtime_dir() let mut path = dirs::runtime_dir()
.unwrap_or_else(|| PathBuf::from("/tmp")); .unwrap_or_else(|| PathBuf::from("/tmp"));
if cfg!(debug_assertions) { path.push(format!("{sock_name}.sock"));
path.push(format!("{sock_name}.dev.sock"))
}
else {
path.push(format!("{sock_name}.sock"));
}
path path
} }
} }
@ -47,31 +35,7 @@ mod platform {
#[cfg(windows)] #[cfg(windows)]
mod platform { mod platform {
use std::path::PathBuf; pub fn server_addr(sock_name: &str) -> String {
use std::time::Duration; format!(r"\\.\pipe\{sock_name}")
use tokio::net::windows::named_pipe::{NamedPipeClient, ClientOptions};
use windows::Win32::Foundation::ERROR_PIPE_BUSY;
pub async fn connect(addr: Option<PathBuf>) -> std::io::Result<NamedPipeClient> {
let opts = ClientOptions::new();
let pipe_name = addr.unwrap_or_else(|| server_addr("creddy-server"));
loop {
match opts.open(&pipe_name) {
Ok(client) => return Ok(client),
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => {
tokio::time::sleep(Duration::from_millis(50)).await;
},
Err(e) => return Err(e),
}
}
}
pub fn server_addr(sock_name: &str) -> PathBuf {
if cfg!(debug_assertions) {
format!(r"\\.\pipe\{sock_name}.dev").into()
}
else {
format!(r"\\.\pipe\{sock_name}").into()
}
} }
} }

View File

@ -1,17 +1,13 @@
use std::env; use std::env;
use std::process::{self, Command}; use std::process::{self, Command};
use creddy_cli::{ use creddy_cli::{Action, Cli};
Action,
Cli,
RunArgs,
};
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let res = match cli.action { let res = match cli.action {
None => launch_gui(RunArgs { minimized: false }), None | Some(Action::Run)=> launch_gui(),
Some(Action::Run(run_args)) => launch_gui(run_args),
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args), Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),
Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args), Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args),
Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args), Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args),
@ -25,7 +21,7 @@ fn main() {
} }
fn launch_gui(run_args: RunArgs) -> anyhow::Result<()> { fn launch_gui() -> anyhow::Result<()> {
let mut path = env::current_exe()?; let mut path = env::current_exe()?;
path.pop(); // bin dir path.pop(); // bin dir
@ -35,10 +31,6 @@ fn launch_gui(run_args: RunArgs) -> anyhow::Result<()> {
path.push("creddy.exe"); // exe in main install dir (aka gui exe) path.push("creddy.exe"); // exe in main install dir (aka gui exe)
let mut cmd = Command::new(path); Command::new(path).spawn()?;
if run_args.minimized {
cmd.arg("--minimized");
}
cmd.spawn()?;
Ok(()) Ok(())
} }

View File

@ -99,8 +99,8 @@ pub struct DockerCredential {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ServerError { pub struct ServerError {
pub code: String, code: String,
pub msg: String, msg: String,
} }
impl Display for ServerError { impl Display for ServerError {

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","os:allow-os-type","dialog:allow-open"]}} {"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","os:allow-os-type","dialog:allow-open"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,5 @@ CREATE TABLE docker_credentials (
server_url TEXT UNIQUE NOT NULL, server_url TEXT UNIQUE NOT NULL,
username TEXT NOT NULL, username TEXT NOT NULL,
secret_enc BLOB NOT NULL, secret_enc BLOB NOT NULL,
nonce BLOB NOT NULL, nonce BLOB NOT NULL
FOREIGN KEY(id) REFERENCES credentials(id) ON DELETE CASCADE
); );

View File

@ -15,7 +15,7 @@ use tauri::{
RunEvent, RunEvent,
WindowEvent, WindowEvent,
}; };
use creddy_cli::{GlobalArgs, RunArgs}; use tauri::menu::MenuItem;
use crate::{ use crate::{
config::{self, AppConfig}, config::{self, AppConfig},
@ -32,13 +32,12 @@ use crate::{
pub static APP: OnceCell<AppHandle> = OnceCell::new(); pub static APP: OnceCell<AppHandle> = OnceCell::new();
pub fn run(run_args: RunArgs, global_args: GlobalArgs) -> tauri::Result<()> { pub fn run() -> tauri::Result<()> {
if let Ok(_) = creddy_cli::show_window(global_args) {
// app is already running, so terminate
return Ok(());
}
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
show_main_window(app)
.error_popup("Failed to show main window")
}))
.plugin(tauri_plugin_global_shortcut::Builder::default().build()) .plugin(tauri_plugin_global_shortcut::Builder::default().build())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
@ -59,10 +58,9 @@ pub fn run(run_args: RunArgs, global_args: GlobalArgs) -> tauri::Result<()> {
ipc::save_config, ipc::save_config,
ipc::launch_terminal, ipc::launch_terminal,
ipc::get_setup_errors, ipc::get_setup_errors,
ipc::get_devmode,
ipc::exit, ipc::exit,
]) ])
.setup(|app| rt::block_on(setup(app, run_args))) .setup(|app| rt::block_on(setup(app)))
.build(tauri::generate_context!())? .build(tauri::generate_context!())?
.run(|app, run_event| { .run(|app, run_event| {
if let RunEvent::WindowEvent { event, .. } = run_event { if let RunEvent::WindowEvent { event, .. } = run_event {
@ -88,11 +86,11 @@ pub async fn connect_db() -> Result<SqlitePool, SetupError> {
} }
async fn setup(app: &mut App, run_args: RunArgs) -> Result<(), Box<dyn Error>> { async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
APP.set(app.handle().clone()).unwrap(); APP.set(app.handle().clone()).unwrap();
tray::setup(app)?; tray::setup(app)?;
// get_or_create_db_path doesn't create the actual db file, just the directory // get_or_create_db_path doesn't create the actual db file, just the directory
let is_first_launch = !config::get_or_create_db_path()?.try_exists()?; let is_first_launch = !config::get_or_create_db_path()?.exists();
let pool = connect_db().await?; let pool = connect_db().await?;
let mut setup_errors: Vec<String> = vec![]; let mut setup_errors: Vec<String> = vec![];
@ -111,16 +109,10 @@ async fn setup(app: &mut App, run_args: RunArgs) -> Result<(), Box<dyn Error>> {
creddy_server::serve(app.handle().clone())?; creddy_server::serve(app.handle().clone())?;
agent::serve(app.handle().clone())?; agent::serve(app.handle().clone())?;
// if this is the first launch, setup system with default auto-launch settings config::set_auto_launch(conf.start_on_login)?;
if is_first_launch { if let Err(_e) = config::set_auto_launch(conf.start_on_login) {
if let Err(e) = conf.set_auto_launch() { setup_errors.push("Error: Failed to manage autolaunch.".into());
setup_errors.push(format!("Failed to manage autolaunch: {e}"));
}
} }
// otherwise, treat the system as the source of truth and ensure ours matches
else {
conf.match_auto_launch(&pool).await?;
};
// if hotkeys fail to register, disable them so that this error doesn't have to keep showing up // if hotkeys fail to register, disable them so that this error doesn't have to keep showing up
if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) { if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) {
@ -133,7 +125,7 @@ async fn setup(app: &mut App, run_args: RunArgs) -> Result<(), Box<dyn Error>> {
.map(|names| names.split(':').any(|n| n == "GNOME")) .map(|names| names.split(':').any(|n| n == "GNOME"))
.unwrap_or(false); .unwrap_or(false);
if !run_args.minimized { if !conf.start_minimized || is_first_launch {
show_main_window(&app.handle())?; show_main_window(&app.handle())?;
} }
@ -166,8 +158,8 @@ fn start_auto_locker(app: AppHandle) {
pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> { pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> {
let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?;
w.show()?; w.show()?;
let menu = app.state::<tray::MenuItems>(); let show_hide = app.state::<MenuItem<tauri::Wry>>();
menu.after_show()?; show_hide.set_text("Hide")?;
Ok(()) Ok(())
} }
@ -175,8 +167,8 @@ pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> {
pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> { pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> {
let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?;
w.hide()?; w.hide()?;
let menu = app.state::<tray::MenuItems>(); let show_hide = app.state::<MenuItem<tauri::Wry>>();
menu.after_hide()?; show_hide.set_text("Show")?;
Ok(()) Ok(())
} }

View File

@ -5,8 +5,7 @@ use sysinfo::{
SystemExt, SystemExt,
Pid, Pid,
PidExt, PidExt,
ProcessExt, ProcessExt
UserExt,
}; };
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -17,16 +16,13 @@ use crate::errors::*;
pub struct Client { pub struct Client {
pub pid: u32, pub pid: u32,
pub exe: Option<PathBuf>, pub exe: Option<PathBuf>,
pub username: Option<String>,
} }
pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> { pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> {
let sys_pid = Pid::from_u32(pid); let sys_pid = Pid::from_u32(pid);
let mut sys = System::new(); let mut sys = System::new();
sys.refresh_process(sys_pid); sys.refresh_process(sys_pid);
sys.refresh_users_list();
let mut proc = sys.process(sys_pid) let mut proc = sys.process(sys_pid)
.ok_or(ClientInfoError::ProcessNotFound)?; .ok_or(ClientInfoError::ProcessNotFound)?;
@ -38,15 +34,10 @@ pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> {
.ok_or(ClientInfoError::ParentProcessNotFound)?; .ok_or(ClientInfoError::ParentProcessNotFound)?;
} }
let username = proc.user_id()
.map(|uid| sys.get_user_by_id(uid))
.flatten()
.map(|u| u.name().to_owned());
let exe = match proc.exe() { let exe = match proc.exe() {
p if p == Path::new("") => None, p if p == Path::new("") => None,
p => Some(PathBuf::from(p)), p => Some(PathBuf::from(p)),
}; };
Ok(Client { pid: proc.pid().as_u32(), exe, username }) Ok(Client { pid: proc.pid().as_u32(), exe })
} }

View File

@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use auto_launch::{AutoLaunch, AutoLaunchBuilder}; use auto_launch::AutoLaunchBuilder;
use is_terminal::IsTerminal; use is_terminal::IsTerminal;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
@ -89,49 +89,29 @@ impl AppConfig {
pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> { pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
kv::save(pool, "config", self).await kv::save(pool, "config", self).await
} }
}
/// Configure system with auto-launch settings
pub fn set_auto_launch(&self) -> Result<(), SetupError> {
let mgr = self.auto_launch_manager()?;
// if enabled, disabled regardless of desired end state because either: pub fn set_auto_launch(is_configured: bool) -> Result<(), SetupError> {
// a) we are just going to leave it disabled, or let path_buf = std::env::current_exe()
// b) we need to disable-and-reenable in case args are different .map_err(|e| auto_launch::Error::Io(e))?;
if mgr.is_enabled()? { let path = path_buf
mgr.disable()?; .to_string_lossy();
}
if self.start_on_login {
mgr.enable()?;
}
Ok(()) 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()?;
} }
/// Match own auto-launch settings to system Ok(())
pub async fn match_auto_launch(&mut self, pool: &SqlitePool) -> Result<(), SetupError> {
let mgr = self.auto_launch_manager()?;
let is_enabled = mgr.is_enabled()?;
if is_enabled != self.start_on_login {
self.start_on_login = is_enabled;
self.save(pool).await?;
}
Ok(())
}
fn auto_launch_manager(&self) -> Result<AutoLaunch, SetupError> {
let path_buf = std::env::current_exe()
.map_err(|e| auto_launch::Error::Io(e))?;
let name = if cfg!(debug_assertions) { "Creddy (dev)" } else { "Creddy" };
let mut builder = AutoLaunchBuilder::new();
builder.set_app_name(name);
builder.set_app_path(&path_buf.to_string_lossy());
if self.start_minimized {
builder.set_args(&["run", "--minimized"]);
}
Ok(builder.build()?)
}
} }

View File

@ -139,10 +139,3 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
Ok(creds) Ok(creds)
} }
} }
pub fn random_uuid() -> Uuid {
// a bit weird to use salt() for this, but it's convenient
let random_bytes = Crypto::salt();
Uuid::from_slice(&random_bytes[..16]).unwrap()
}

View File

@ -201,10 +201,6 @@ pub enum HandlerError {
Signature(#[from] signature::Error), Signature(#[from] signature::Error),
#[error(transparent)] #[error(transparent)]
Encoding(#[from] ssh_encoding::Error), Encoding(#[from] ssh_encoding::Error),
#[cfg(windows)]
#[error(transparent)]
Windows(#[from] windows::core::Error),
} }

View File

@ -14,14 +14,6 @@ use crate::state::AppState;
use crate::terminal; use crate::terminal;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum RequestAction {
Access,
Delete,
Save,
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AwsRequestNotification { pub struct AwsRequestNotification {
pub client: Client, pub client: Client,
@ -39,7 +31,6 @@ pub struct SshRequestNotification {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DockerRequestNotification { pub struct DockerRequestNotification {
pub action: RequestAction,
pub client: Client, pub client: Client,
pub server_url: String, pub server_url: String,
} }
@ -62,8 +53,8 @@ impl RequestNotificationDetail {
Self::Ssh(SshRequestNotification {client, key_name}) Self::Ssh(SshRequestNotification {client, key_name})
} }
pub fn new_docker(action: RequestAction, client: Client, server_url: String) -> Self { pub fn new_docker(client: Client, server_url: String) -> Self {
Self::Docker(DockerRequestNotification {action, client, server_url}) Self::Docker(DockerRequestNotification {client, server_url})
} }
} }
@ -204,12 +195,6 @@ pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<Stri
} }
#[tauri::command]
pub fn get_devmode() -> bool {
cfg!(debug_assertions)
}
#[tauri::command] #[tauri::command]
pub fn exit(app_handle: AppHandle) { pub fn exit(app_handle: AppHandle) {
app_handle.exit(0) app_handle.exit(0)

View File

@ -8,23 +8,14 @@ use creddy::{
app, app,
errors::ShowError, errors::ShowError,
}; };
use creddy_cli::{ use creddy_cli::{Action, Cli};
Action,
Cli,
RunArgs,
};
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let res = match cli.action { let res = match cli.action {
None => { None | Some(Action::Run) => {
let run_args = RunArgs { minimized: false }; app::run().error_popup("Creddy encountered an error");
app::run(run_args, cli.global_args).error_popup("Creddy encountered an error");
Ok(())
}
Some(Action::Run(run_args)) => {
app::run(run_args, cli.global_args).error_popup("Creddy encountered an error");
Ok(()) Ok(())
}, },
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args), Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),

View File

@ -44,7 +44,10 @@ fn launch_terminal() {
pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> Result<(), ShortcutError> { pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> Result<(), ShortcutError> {
let app = APP.get().unwrap(); let app = APP.get().unwrap();
let shortcuts = app.global_shortcut(); let shortcuts = app.global_shortcut();
shortcuts.unregister_all()?; shortcuts.unregister_all([
hotkeys.show_window.keys.as_str(),
hotkeys.launch_terminal.keys.as_str(),
])?;
if hotkeys.show_window.enabled { if hotkeys.show_window.enabled {
shortcuts.on_shortcut( shortcuts.on_shortcut(

View File

@ -6,11 +6,12 @@ use ssh_agent_lib::proto::message::{
}; };
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tokio::sync::oneshot;
use tokio_util::codec::Framed; use tokio_util::codec::Framed;
use crate::clientinfo; use crate::clientinfo;
use crate::errors::*; use crate::errors::*;
use crate::ipc::{Approval, RequestNotificationDetail}; use crate::ipc::{Approval, RequestNotification, RequestNotificationDetail};
use crate::state::AppState; use crate::state::AppState;
use super::{CloseWaiter, Stream}; use super::{CloseWaiter, Stream};

View File

@ -1,19 +1,16 @@
use sqlx::types::uuid::Uuid;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::oneshot;
use crate::clientinfo::{self, Client}; use crate::clientinfo::{self, Client};
use crate::credentials::{ use crate::credentials::{
self,
Credential, Credential,
CredentialRecord, CredentialRecord,
DockerCredential, Crypto
}; };
use crate::errors::*; use crate::errors::*;
use crate::ipc::{ use crate::ipc::{Approval, AwsRequestNotification, RequestNotificationDetail, RequestResponse};
Approval,
RequestAction,
RequestNotificationDetail
};
use crate::shortcuts::{self, ShortcutAction}; use crate::shortcuts::{self, ShortcutAction};
use crate::state::AppState; use crate::state::AppState;
use super::{ use super::{
@ -58,16 +55,13 @@ async fn handle(
CliRequest::GetAwsCredential{ name, base } => get_aws_credentials( CliRequest::GetAwsCredential{ name, base } => get_aws_credentials(
name, base, client, app_handle, waiter name, base, client, app_handle, waiter
).await, ).await,
CliRequest::GetDockerCredential{ server_url } => get_docker_credential ( CliRequest::GetDockerCredential{ server_url } => get_docker_credentials (
server_url, client, app_handle, waiter server_url, client, app_handle, waiter
).await, ).await,
CliRequest::StoreDockerCredential(docker_credential) => store_docker_credential( CliRequest::SaveCredential{ name, is_default, credential } => save_credential(
docker_credential, app_handle, client, waiter name, is_default, credential, app_handle
).await, ).await,
CliRequest::EraseDockerCredential { server_url } => erase_docker_credential( CliRequest::InvokeShortcut(action) => invoke_shortcut(action).await,
server_url, app_handle, client, waiter
).await,
CliRequest::InvokeShortcut{ action } => invoke_shortcut(action).await,
}; };
// doesn't make sense to send the error to the client if the client has already left // doesn't make sense to send the error to the client if the client has already left
@ -112,32 +106,17 @@ async fn get_aws_credentials(
} }
} }
async fn get_docker_credential( async fn get_docker_credentials(
server_url: String, server_url: String,
client: Client, client: Client,
app_handle: AppHandle, app_handle: AppHandle,
waiter: CloseWaiter<'_>, waiter: CloseWaiter<'_>,
) -> Result<CliResponse, HandlerError> { ) -> Result<CliResponse, HandlerError> {
let state = app_handle.state::<AppState>(); let detail = RequestNotificationDetail::new_docker(client, server_url.clone());
let meta = state.docker_credential_meta(&server_url).await.unwrap_or(None);
if meta.is_none() {
return Err(
HandlerError::NoCredentials(
GetCredentialsError::Load(
LoadCredentialsError::NoCredentials
)
)
);
}
let detail = RequestNotificationDetail::new_docker(
RequestAction::Access,
client,
server_url.clone()
);
let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?;
match response.approval { match response.approval {
Approval::Approved => { Approval::Approved => {
let state = app_handle.state::<AppState>();
let creds = state.get_docker_credential(&server_url).await?; let creds = state.get_docker_credential(&server_url).await?;
Ok(CliResponse::Credential(CliCredential::Docker(creds))) Ok(CliResponse::Credential(CliCredential::Docker(creds)))
}, },
@ -147,77 +126,24 @@ async fn get_docker_credential(
} }
} }
async fn store_docker_credential( pub async fn save_credential(
docker_credential: DockerCredential, name: String,
is_default: bool,
credential: Credential,
app_handle: AppHandle, app_handle: AppHandle,
client: Client,
waiter: CloseWaiter<'_>,
) -> Result<CliResponse, HandlerError> { ) -> Result<CliResponse, HandlerError> {
let state = app_handle.state::<AppState>(); let state = app_handle.state::<AppState>();
// We want to do this before asking for confirmation from the user, because Docker has an annoying // eventually ask the frontend to unlock here
// habit of calling `get` and then immediately turning around and calling `store` with the same
// data. In that case we want to avoid asking for confirmation at all.
match state.get_docker_credential(&docker_credential.server_url).await {
// if there is already a credential with this server_url, and it is unchanged, we're done
Ok(c) if c == docker_credential => return Ok(CliResponse::Empty),
// otherwise we are making an update, so proceed
Ok(_) => (),
// if the app is locked, then this isn't the situation described above, so proceed
Err(GetCredentialsError::Locked) => (),
// if the app is unlocked, and there is no matching credential, proceed
Err(GetCredentialsError::Load(LoadCredentialsError::NoCredentials)) => (),
// any other error is a failure
Err(e) => return Err(e.into()),
};
let detail = RequestNotificationDetail::new_docker( // a bit weird but convenient
RequestAction::Save, let random_bytes = Crypto::salt();
client, let id = Uuid::from_slice(&random_bytes[..16]).unwrap();
docker_credential.server_url.clone(),
);
let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?;
if matches!(response.approval, Approval::Denied) {
return Err(HandlerError::Denied);
}
let (id, name) = state.docker_credential_meta(&docker_credential.server_url)
.await
.map_err(|e| GetCredentialsError::Load(e))?
.unwrap_or_else(|| (credentials::random_uuid(), docker_credential.server_url.clone()));
let record = CredentialRecord { let record = CredentialRecord {
id, id, name, is_default, credential
name,
is_default: false,
credential: Credential::Docker(docker_credential)
}; };
state.save_credential(record).await?; state.save_credential(record).await?;
Ok(CliResponse::Empty) Ok(CliResponse::Empty)
} }
async fn erase_docker_credential(
server_url: String,
app_handle: AppHandle,
client: Client,
waiter: CloseWaiter<'_>
) -> Result<CliResponse, HandlerError> {
let state = app_handle.state::<AppState>();
let detail = RequestNotificationDetail::new_docker(
RequestAction::Delete,
client,
server_url.clone(),
);
let resp = super::send_credentials_request(detail, app_handle.clone(), waiter).await?;
match resp.approval {
Approval::Approved => {
state.delete_credential_by_name(&server_url).await?;
Ok(CliResponse::Empty)
}
Approval::Denied => {
Err(HandlerError::Denied)
}
}
}

View File

@ -3,17 +3,17 @@ use std::future::Future;
use tauri::{ use tauri::{
AppHandle, AppHandle,
async_runtime as rt, async_runtime as rt,
Emitter,
Manager, Manager,
Runtime,
}; };
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::clientinfo::Client;
use crate::credentials::{ use crate::credentials::{
AwsBaseCredential, AwsBaseCredential,
AwsSessionCredential, AwsSessionCredential,
Credential,
DockerCredential, DockerCredential,
}; };
use crate::errors::*; use crate::errors::*;
@ -30,7 +30,6 @@ use platform::Stream;
// so that we avoid polluting the standalone CLI with a bunch of dependencies // so that we avoid polluting the standalone CLI with a bunch of dependencies
// that would make it impossible to build a completely static-linked version // that would make it impossible to build a completely static-linked version
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum CliRequest { pub enum CliRequest {
GetAwsCredential { GetAwsCredential {
name: Option<String>, name: Option<String>,
@ -39,13 +38,12 @@ pub enum CliRequest {
GetDockerCredential { GetDockerCredential {
server_url: String, server_url: String,
}, },
StoreDockerCredential(DockerCredential), SaveCredential {
EraseDockerCredential { name: String,
server_url: String, is_default: bool,
}, credential: Credential,
InvokeShortcut{
action: ShortcutAction,
}, },
InvokeShortcut(ShortcutAction),
} }
@ -82,11 +80,9 @@ impl<'s> CloseWaiter<'s> {
} }
// note: AppHandle is generic over `Runtime` for testing fn serve<H, F>(sock_name: &str, app_handle: AppHandle, handler: H) -> std::io::Result<()>
fn serve<H, F, R>(sock_name: &str, app_handle: AppHandle<R>, handler: H) -> std::io::Result<()> where H: Copy + Send + Fn(Stream, AppHandle, u32) -> F + 'static,
where H: Copy + Send + Fn(Stream, AppHandle<R>, u32) -> F + 'static,
F: Send + Future<Output = Result<(), HandlerError>>, F: Send + Future<Output = Result<(), HandlerError>>,
R: Runtime
{ {
let (mut listener, addr) = platform::bind(sock_name)?; let (mut listener, addr) = platform::bind(sock_name)?;
rt::spawn(async move { rt::spawn(async move {
@ -189,7 +185,6 @@ mod platform {
#[cfg(windows)] #[cfg(windows)]
mod platform { mod platform {
use std::os::windows::io::AsRawHandle; use std::os::windows::io::AsRawHandle;
use std::path::PathBuf;
use tokio::net::windows::named_pipe::{ use tokio::net::windows::named_pipe::{
NamedPipeServer, NamedPipeServer,
ServerOptions, ServerOptions,
@ -203,7 +198,7 @@ mod platform {
pub type Stream = NamedPipeServer; pub type Stream = NamedPipeServer;
pub fn bind(sock_name: &str) -> std::io::Result<(NamedPipeServer, PathBuf)> { pub fn bind(sock_name: &str) -> std::io::Result<(String, NamedPipeServer)> {
let addr = creddy_cli::server_addr(sock_name); let addr = creddy_cli::server_addr(sock_name);
let listener = ServerOptions::new() let listener = ServerOptions::new()
.first_pipe_instance(true) .first_pipe_instance(true)
@ -211,7 +206,7 @@ mod platform {
Ok((listener, addr)) Ok((listener, addr))
} }
pub async fn accept(listener: &mut NamedPipeServer, addr: &PathBuf) -> Result<(NamedPipeServer, u32), HandlerError> { pub async fn accept(listener: &mut NamedPipeServer, addr: &String) -> Result<(NamedPipeServer, u32), HandlerError> {
// connect() just waits for a client to connect, it doesn't return anything // connect() just waits for a client to connect, it doesn't return anything
listener.connect().await?; listener.connect().await?;
@ -228,31 +223,3 @@ mod platform {
Ok((stream, pid)) Ok((stream, pid))
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn test_server_connect() {
let app = tauri::test::mock_app();
serve("creddy_server_test", app.app_handle().clone(), |mut stream, _handle, _pid| {
async move {
let buf = serde_json::to_vec(&CliResponse::Empty).unwrap();
stream.write_all(&buf).await.unwrap();
Ok(())
}
}).unwrap();
let addr = creddy_cli::server_addr("creddy_server_test");
let mut stream = creddy_cli::connect(Some(addr)).await.unwrap();
let mut buf = Vec::new();
stream.read_to_end(&mut buf).await.unwrap();
let resp: CliResponse = serde_json::from_slice(&buf).unwrap();
assert!(matches!(resp, CliResponse::Empty))
}
}

View File

@ -11,7 +11,6 @@ use ssh_agent_lib::proto::message::Identity;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use sqlx::types::Uuid; use sqlx::types::Uuid;
use tauri::{ use tauri::{
Emitter,
Manager, Manager,
async_runtime as rt, async_runtime as rt,
}; };
@ -23,7 +22,7 @@ use crate::credentials::{
DockerCredential, DockerCredential,
SshKey, SshKey,
}; };
use crate::config::AppConfig; use crate::{config, config::AppConfig};
use crate::credentials::{ use crate::credentials::{
AwsBaseCredential, AwsBaseCredential,
Credential, Credential,
@ -33,7 +32,6 @@ use crate::credentials::{
use crate::ipc::{self, RequestResponse}; use crate::ipc::{self, RequestResponse};
use crate::errors::*; use crate::errors::*;
use crate::shortcuts; use crate::shortcuts;
use crate::tray;
#[derive(Debug)] #[derive(Debug)]
@ -163,13 +161,6 @@ impl AppState {
Ok(()) Ok(())
} }
pub async fn delete_credential_by_name(&self, name: &str) -> Result<(), SaveCredentialsError> {
sqlx::query!("DELETE FROM credentials WHERE name = ?", name)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn list_credentials(&self) -> Result<Vec<CredentialRecord>, GetCredentialsError> { pub async fn list_credentials(&self) -> Result<Vec<CredentialRecord>, GetCredentialsError> {
let session = self.app_session.read().await; let session = self.app_session.read().await;
let crypto = session.try_get_crypto()?; let crypto = session.try_get_crypto()?;
@ -205,9 +196,8 @@ impl AppState {
let mut live_config = self.config.write().await; let mut live_config = self.config.write().await;
// update autostart if necessary // update autostart if necessary
if new_config.start_on_login != live_config.start_on_login if new_config.start_on_login != live_config.start_on_login {
|| new_config.start_minimized != live_config.start_minimized { config::set_auto_launch(new_config.start_on_login)?;
new_config.set_auto_launch()?;
} }
// re-register hotkeys if necessary // re-register hotkeys if necessary
@ -255,11 +245,7 @@ impl AppState {
pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> { pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
let mut session = self.app_session.write().await; let mut session = self.app_session.write().await;
session.unlock(passphrase)?; session.unlock(passphrase)
let app_handle = app::APP.get().unwrap();
let menu = app_handle.state::<tray::MenuItems>();
let _ = menu.after_unlock(); // we don't care if this fails, it's non-essential
Ok(())
} }
pub async fn lock(&self) -> Result<(), LockError> { pub async fn lock(&self) -> Result<(), LockError> {
@ -273,9 +259,6 @@ impl AppState {
let app_handle = app::APP.get().unwrap(); let app_handle = app::APP.get().unwrap();
app_handle.emit("locked", None::<usize>)?; app_handle.emit("locked", None::<usize>)?;
let menu = app_handle.state::<tray::MenuItems>();
let _ = menu.after_lock();
Ok(()) Ok(())
} }
} }
@ -340,23 +323,6 @@ impl AppState {
Ok(k) Ok(k)
} }
pub async fn docker_credential_meta(
&self, server_url: &str
) -> Result<Option<(Uuid, String)>, LoadCredentialsError> {
let res = sqlx::query!(
r#"SELECT
c.id as "id: Uuid",
c.name
FROM
credentials c
JOIN docker_credentials d
ON d.id = c.id
WHERE d.server_url = ?"#,
server_url
).fetch_optional(&self.pool).await?;
Ok(res.map(|row| (row.id, row.name)))
}
pub async fn get_docker_credential(&self, server_url: &str) -> Result<DockerCredential, GetCredentialsError> { pub async fn get_docker_credential(&self, server_url: &str) -> Result<DockerCredential, GetCredentialsError> {
let app_session = self.app_session.read().await; let app_session = self.app_session.read().await;
let crypto = app_session.try_get_crypto()?; let crypto = app_session.try_get_crypto()?;

View File

@ -1,11 +1,7 @@
use std::process::Command; use std::process::Command;
use std::time::Duration; use std::time::Duration;
use tauri::{ use tauri::{AppHandle, Manager};
AppHandle,
Listener,
Manager,
};
use tokio::time::sleep; use tokio::time::sleep;
use crate::app::APP; use crate::app::APP;

View File

@ -7,78 +7,27 @@ use tauri::{
use tauri::menu::{ use tauri::menu::{
MenuBuilder, MenuBuilder,
MenuEvent, MenuEvent,
MenuItem,
MenuItemBuilder, MenuItemBuilder,
PredefinedMenuItem,
}; };
use tauri::tray::TrayIconBuilder;
use crate::app; use crate::app;
use crate::state::AppState; use crate::state::AppState;
pub struct MenuItems {
pub status: MenuItem<tauri::Wry>,
pub show_hide: MenuItem<tauri::Wry>,
}
impl MenuItems {
pub fn after_show(&self) -> tauri::Result<()> {
self.show_hide.set_text("Hide")
}
pub fn after_hide(&self) -> tauri::Result<()> {
self.show_hide.set_text("Show")
}
pub fn after_lock(&self) -> tauri::Result<()> {
if cfg!(debug_assertions) {
self.status.set_text("Creddy (dev): Locked")
}
else {
self.status.set_text("Creddy: Locked")
}
}
pub fn after_unlock(&self) -> tauri::Result<()> {
if cfg!(debug_assertions) {
self.status.set_text("Creddy (dev): Unlocked")
}
else {
self.status.set_text("Creddy: Unlocked")
}
}
}
pub fn setup(app: &App) -> tauri::Result<()> { pub fn setup(app: &App) -> tauri::Result<()> {
let status_text =
if cfg!(debug_assertions) {
"Creddy (dev): Locked"
}
else {
"Creddy: Locked"
};
let status = MenuItemBuilder::with_id("status", status_text)
.enabled(false)
.build(app)?;
let sep = PredefinedMenuItem::separator(app)?;
let show_hide = MenuItemBuilder::with_id("show_hide", "Show").build(app)?; let show_hide = MenuItemBuilder::with_id("show_hide", "Show").build(app)?;
let exit = MenuItemBuilder::with_id("exit", "Exit").build(app)?; let exit = MenuItemBuilder::with_id("exit", "Exit").build(app)?;
let menu = MenuBuilder::new(app) let menu = MenuBuilder::new(app)
.items(&[&status, &sep, &show_hide, &exit]) .items(&[&show_hide, &exit])
.build()?; .build()?;
TrayIconBuilder::new() let tray = app.tray_by_id("main").unwrap();
.icon(app.default_window_icon().unwrap().clone()) tray.set_menu(Some(menu))?;
.menu(&menu) tray.on_menu_event(handle_event);
.on_menu_event(handle_event)
.build(app)?;
// stash these so we can find them later to change the text // stash this so we can find it later to change the text
app.manage(MenuItems { status, show_hide }); app.manage(show_hide);
Ok(()) Ok(())
} }

View File

@ -50,7 +50,7 @@
} }
}, },
"productName": "creddy", "productName": "creddy",
"version": "0.6.5", "version": "0.5.4",
"identifier": "creddy", "identifier": "creddy",
"plugins": {}, "plugins": {},
"app": { "app": {
@ -65,6 +65,11 @@
"visible": false "visible": false
} }
], ],
"trayIcon": {
"id": "main",
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"security": { "security": {
"csp": { "csp": {
"style-src": [ "style-src": [

View File

@ -14,7 +14,6 @@ import Unlock from './views/Unlock.svelte';
// set up app state // set up app state
invoke('get_config').then(config => $appState.config = config); invoke('get_config').then(config => $appState.config = config);
invoke('get_session_status').then(status => $appState.sessionStatus = status); invoke('get_session_status').then(status => $appState.sessionStatus = status);
invoke('get_devmode').then(dm => $appState.devmode = dm)
getVersion().then(version => $appState.appVersion = version); getVersion().then(version => $appState.appVersion = version);
invoke('get_setup_errors') invoke('get_setup_errors')
.then(errs => { .then(errs => {
@ -52,7 +51,7 @@ acceptRequest();
</script> </script>
<svelte:window <svelte:window
on:click={() => invoke('signal_activity')} on:click={() => invoke('signal_activity')}
on:keydown={() => invoke('signal_activity')} on:keydown={() => invoke('signal_activity')}
/> />
@ -71,9 +70,3 @@ acceptRequest();
<!-- normal operation --> <!-- normal operation -->
<svelte:component this="{$currentView}" /> <svelte:component this="{$currentView}" />
{/if} {/if}
{#if $appState.devmode }
<div class="fixed left-0 bottom-0 right-0 py-1 bg-warning text-xs text-center text-warning-content">
This is a development build of Creddy.
</div>
{/if}

View File

@ -1,7 +1,7 @@
<script> <script>
// import { listen } from '@tauri-apps/api/event'; // import { listen } from '@tauri-apps/api/event';
import { open } from '@tauri-apps/plugin-dialog'; import { open } from '@tauri-apps/plugin-dialog';
import { basename } from '@tauri-apps/api/path'; import { sep } from '@tauri-apps/api/path';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Icon from './Icon.svelte'; import Icon from './Icon.svelte';
@ -14,16 +14,17 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function chooseFile() { async function chooseFile() {
let path = await open(params); let file = await open(params);
if (path) { if (file) {
displayValue = await basename(path); value = file;
value = {name: displayValue, path}; displayValue = file.name;
dispatch('update', value); dispatch('update', value);
} }
} }
async function handleInput(evt) { function handleInput(evt) {
const name = await basename(evt.target.value); const segments = evt.target.value.split(sep());
const name = segments[segments.length - 1];
value = {name, path: evt.target.value}; value = {name, path: evt.target.value};
} }

View File

@ -4,10 +4,10 @@
export let value = ''; export let value = '';
export let placeholder = ''; export let placeholder = '';
export let autofocus = false; export let autofocus = false;
export let show = false;
let classes = ''; let classes = '';
export {classes as class}; export {classes as class};
let show = false;
let input; let input;
export function focus() { export function focus() {

View File

@ -7,7 +7,6 @@
import ShowResponse from './approve/ShowResponse.svelte'; import ShowResponse from './approve/ShowResponse.svelte';
import Unlock from './Unlock.svelte'; import Unlock from './Unlock.svelte';
console.log($appState.currentRequest);
// Extra 50ms so the window can finish disappearing before the redraw // Extra 50ms so the window can finish disappearing before the redraw
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100); const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100);

View File

@ -91,7 +91,7 @@
{#if launchTerminalError} {#if launchTerminalError}
<div class="toast"> <div class="toast">
<div class="alert alert-error text-wrap shadow-lg"> <div class="alert alert-error shadow-lg">
<span>{launchTerminalError.msg || launchTerminalError}</span> <span>{launchTerminalError.msg || launchTerminalError}</span>
<div> <div>
<button class="btn btn-alert-error" on:click={() => launchTerminalError = null}> <button class="btn btn-alert-error" on:click={() => launchTerminalError = null}>

View File

@ -6,8 +6,9 @@
import AwsCredential from './credentials/AwsCredential.svelte'; import AwsCredential from './credentials/AwsCredential.svelte';
import ConfirmDelete from './credentials/ConfirmDelete.svelte'; import ConfirmDelete from './credentials/ConfirmDelete.svelte';
import DockerCredential from './credentials/DockerCredential.svelte';
import SshKey from './credentials/SshKey.svelte'; import SshKey from './credentials/SshKey.svelte';
// import NewSshKey from './credentials/NewSshKey.svelte';
// import EditSshKey from './credentials/EditSshKey.svelte';
import Icon from '../ui/Icon.svelte'; import Icon from '../ui/Icon.svelte';
import Nav from '../ui/Nav.svelte'; import Nav from '../ui/Nav.svelte';
@ -15,7 +16,6 @@
let records = null let records = null
$: awsRecords = (records || []).filter(r => r.credential.type === 'AwsBase'); $: awsRecords = (records || []).filter(r => r.credential.type === 'AwsBase');
$: sshRecords = (records || []).filter(r => r.credential.type === 'Ssh'); $: sshRecords = (records || []).filter(r => r.credential.type === 'Ssh');
$: dockerRecords = (records || []).filter(r => r.credential.type === 'Docker');
let defaults = writable({}); let defaults = writable({});
async function loadCreds() { async function loadCreds() {
@ -47,17 +47,6 @@
records = records; records = records;
} }
function newDocker() {
records.push({
id: crypto.randomUUID(),
name: null,
is_default: false,
credential: {type: 'Docker', ServerURL: '', Username: '', Secret: ''},
isNew: true,
});
records = records;
}
let confirmDelete; let confirmDelete;
function handleDelete(evt) { function handleDelete(evt) {
const record = evt.detail; const record = evt.detail;
@ -128,29 +117,6 @@
{/if} {/if}
</div> </div>
<div class="flex flex-col gap-y-4">
<div class="divider">
<h2 class="text-xl font-bold">Docker credentials</h2>
</div>
{#if dockerRecords.length > 0}
{#each dockerRecords as record (record.id)}
<DockerCredential {record} on:save={loadCreds} on:delete={handleDelete} />
{/each}
<button class="btn btn-primary btn-wide mx-auto" on:click={newDocker}>
<Icon name="plus-circle-mini" class="size-5" />
Add
</button>
{:else if records !== null}
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
<div>You have no saved Docker credentials.</div>
<button class="btn btn-primary btn-wide mx-auto" on:click={newDocker}>
<Icon name="plus-circle-mini" class="size-5" />
Add
</button>
</div>
{/if}
</div>
</div> </div>
<ConfirmDelete bind:this={confirmDelete} on:confirm={loadCreds} /> <ConfirmDelete bind:this={confirmDelete} on:confirm={loadCreds} />

View File

@ -20,6 +20,7 @@
let error = null; let error = null;
async function save() { async function save() {
try { try {
throw('wtf');
await invoke('save_config', {config}); await invoke('save_config', {config});
$appState.config = await invoke('get_config'); $appState.config = await invoke('get_config');
} }
@ -40,20 +41,18 @@
<form on:submit|preventDefault={save}> <form on:submit|preventDefault={save}>
<div class="max-w-lg mx-auto my-1.5 p-4 space-y-16"> <div class="max-w-lg mx-auto my-1.5 p-4 space-y-16">
<SettingsGroup name="General"> <SettingsGroup name="General">
<ToggleSetting title="Start on login" bind:value={config.start_on_login}> <ToggleSetting title="Start on login" bind:value={config.start_on_login}>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Start Creddy when you log in to your computer. Start Creddy when you log in to your computer.
</svelte:fragment> </svelte:fragment>
</ToggleSetting> </ToggleSetting>
{#if config.start_on_login} <ToggleSetting title="Start minimized" bind:value={config.start_minimized}>
<ToggleSetting title="Start minimized" bind:value={config.start_minimized}> <svelte:fragment slot="description">
<svelte:fragment slot="description"> Minimize to the system tray at startup.
Minimize to the system tray when starting on login. </svelte:fragment>
</svelte:fragment> </ToggleSetting>
</ToggleSetting>
{/if}
<NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds"> <NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds">
<svelte:fragment slot="description"> <svelte:fragment slot="description">
@ -115,7 +114,7 @@
{#if error} {#if error}
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast"> <div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
<div class="alert alert-error no-animation text-wrap"> <div class="alert alert-error no-animation">
<div> <div>
<span>{error}</span> <span>{error}</span>
</div> </div>
@ -127,7 +126,7 @@
</div> </div>
{:else if configModified} {:else if configModified}
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast"> <div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
<div class="alert shadow-lg no-animation text-wrap"> <div class="alert shadow-lg no-animation">
<span>You have unsaved changes.</span> <span>You have unsaved changes.</span>
<div> <div>

View File

@ -14,7 +14,7 @@
// Extract executable name from full path // Extract executable name from full path
const client = $appState.currentRequest.client; const client = $appState.currentRequest.client;
const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/); const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/);
const appName = m ? m[1] || m[2] : ''; const appName = m[1] || m[2];
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -26,12 +26,6 @@
}; };
dispatch('response'); dispatch('response');
} }
const actionDescriptions = {
Access: 'access your',
Delete: 'delete your',
Save: 'create new',
};
</script> </script>
@ -58,7 +52,7 @@
{:else if $appState.currentRequest.type === 'Ssh'} {:else if $appState.currentRequest.type === 'Ssh'}
{appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}".
{:else if $appState.currentRequest.type === 'Docker'} {:else if $appState.currentRequest.type === 'Docker'}
{appName ? `"${appName}"` : 'An application'} would like to {actionDescriptions[$appState.currentRequest.action]} Docker credentials for <code>{$appState.currentRequest.server_url}</code>. {appName ? `"${appName}"` : 'An application'} would like to use your Docker credentials for <code>{$appState.currentRequest.server_url}</code>.
{/if} {/if}
</h2> </h2>
@ -67,8 +61,6 @@
<code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code> <code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code>
<div class="text-right">PID:</div> <div class="text-right">PID:</div>
<code>{client.pid}</code> <code>{client.pid}</code>
<div class="text-right">User:</div>
<code>{client.username ?? 'Unknown'}</code>
</div> </div>
</div> </div>

View File

@ -5,19 +5,20 @@
import ErrorAlert from '../../ui/ErrorAlert.svelte'; import ErrorAlert from '../../ui/ErrorAlert.svelte';
import Icon from '../../ui/Icon.svelte'; import Icon from '../../ui/Icon.svelte';
import PassphraseInput from '../../ui/PassphraseInput.svelte';
export let record; export let record;
export let defaults; export let defaults;
import PassphraseInput from '../../ui/PassphraseInput.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let showDetails = record.isNew ? true : false; let showDetails = record.isNew ? true : false;
let local = JSON.parse(JSON.stringify(record)); let local = JSON.parse(JSON.stringify(record));
$: isModified = JSON.stringify(local) !== JSON.stringify(record); $: isModified = JSON.stringify(local) !== JSON.stringify(record);
// explicitly subscribe to updates to `default`, so that we can update // explicitly subscribe to updates to `default`, so that we can update
// our local copy even if the component hasn't been recreated // our local copy even if the component hasn't been recreated
// (sadly we can't use a reactive binding because reasons I guess) // (sadly we can't use a reactive binding because reasons I guess)
@ -30,7 +31,7 @@
showDetails = false; showDetails = false;
} }
</script> </script>

View File

@ -26,12 +26,9 @@
if (record.credential.type === 'AwsBase') { if (record.credential.type === 'AwsBase') {
return 'AWS credential'; return 'AWS credential';
} }
else if (record.credential.type === 'Ssh') { if (record.credential.type === 'Ssh') {
return 'SSH key'; return 'SSH key';
} }
else {
return `${record.credential.type} credential`;
}
} }
</script> </script>

View File

@ -1,112 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
import { fade, slide } from 'svelte/transition';
import { invoke } from '@tauri-apps/api/core';
import ErrorAlert from '../../ui/ErrorAlert.svelte';
import Icon from '../../ui/Icon.svelte';
import PassphraseInput from '../../ui/PassphraseInput.svelte';
export let record;
let local = JSON.parse(JSON.stringify(record));
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
let showDetails = record?.isNew;
let alert;
const dispatch = createEventDispatcher();
async function saveCredential() {
await invoke('save_credential', {record: local});
dispatch('save', local);
showDetails = false;
}
</script>
<div class="rounded-box space-y-4 bg-base-200">
<div class="flex items-center px-6 py-4 gap-x-4">
{#if !record.isNew}
{#if showDetails}
<input
type="text"
class="input input-bordered bg-transparent text-lg font-bold grow"
bind:value={local.name}
>
{:else}
<h3 class="text-lg font-bold break-all">
{record.name}
</h3>
{/if}
{/if}
<div class="join ml-auto">
<button
type="button"
class="btn btn-outline join-item"
on:click={() => showDetails = !showDetails}
>
<Icon name="pencil" class="size-6" />
</button>
<button
type="button"
class="btn btn-outline btn-error join-item"
on:click={() => dispatch('delete', record)}
>
<Icon name="trash" class="size-6" />
</button>
</div>
</div>
{#if showDetails}
<form
transition:slide|local={{duration: 200}}
class=" px-6 pb-4 space-y-4"
on:submit|preventDefault={() => alert.run(saveCredential)}
>
<ErrorAlert bind:this={alert} />
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
{#if record.isNew}
<span class="justify-self-end">Name</span>
<input
type="text"
class="input input-bordered bg-transparent"
bind:value={local.name}
>
{/if}
<span class="justify-self-end">Server URL</span>
<input
type="text"
class="input input-bordered font-mono bg-transparent"
bind:value={local.credential.ServerURL}
>
<span class="justify-self-end">Username</span>
<input
type="text"
class="input input-bordered font-mono bg-transparent"
bind:value={local.credential.Username}
>
<span>Password</span>
<div class="font-mono">
<PassphraseInput class="bg-transparent" bind:value={local.credential.Secret} />
</div>
</div>
<div class="flex justify-end">
{#if isModified}
<button
transition:fade={{duration: 100}}
type="submit"
class="btn btn-primary"
>
Save
</button>
{/if}
</div>
</form>
{/if}
</div>

View File

@ -14,7 +14,6 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let showPassphrase = false;
let alert; let alert;
let saving = false; let saving = false;
let passphrase = ''; let passphrase = '';
@ -53,6 +52,7 @@
try { try {
await alert.run(async () => { await alert.run(async () => {
await invoke('set_passphrase', {passphrase}) await invoke('set_passphrase', {passphrase})
throw('something bad happened');
$appState.sessionStatus = 'unlocked'; $appState.sessionStatus = 'unlocked';
dispatch('save'); dispatch('save');
}); });
@ -73,7 +73,6 @@
</div> </div>
<PassphraseInput <PassphraseInput
bind:value={passphrase} bind:value={passphrase}
bind:show={showPassphrase}
on:input={onInput} on:input={onInput}
placeholder="correct horse battery staple" placeholder="correct horse battery staple"
/> />
@ -85,7 +84,6 @@
</div> </div>
<PassphraseInput <PassphraseInput
bind:value={confirmPassphrase} bind:value={confirmPassphrase}
bind:show={showPassphrase}
on:input={onInput} on:change={onChange} on:input={onInput} on:change={onChange}
placeholder="correct horse battery staple" placeholder="correct horse battery staple"
/> />