Compare commits
4 Commits
ce7d75f15a
...
generalize
Author | SHA1 | Date | |
---|---|---|---|
acc5c71bfa | |||
504c0b4156 | |||
bf0a2ca72d | |||
bb980c5eef |
@ -46,6 +46,7 @@ pub fn run() -> tauri::Result<()> {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
ipc::unlock,
|
ipc::unlock,
|
||||||
ipc::lock,
|
ipc::lock,
|
||||||
|
ipc::reset_session,
|
||||||
ipc::set_passphrase,
|
ipc::set_passphrase,
|
||||||
ipc::respond,
|
ipc::respond,
|
||||||
ipc::get_session_status,
|
ipc::get_session_status,
|
||||||
@ -57,6 +58,7 @@ pub fn run() -> tauri::Result<()> {
|
|||||||
ipc::save_config,
|
ipc::save_config,
|
||||||
ipc::launch_terminal,
|
ipc::launch_terminal,
|
||||||
ipc::get_setup_errors,
|
ipc::get_setup_errors,
|
||||||
|
ipc::exit,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let res = rt::block_on(setup(app));
|
let res = rt::block_on(setup(app));
|
||||||
|
@ -18,14 +18,14 @@ use sqlx::{
|
|||||||
types::Uuid,
|
types::Uuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Crypto, PersistentCredential};
|
use super::{Credential, Crypto, PersistentCredential};
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromRow)]
|
#[derive(Debug, Clone, FromRow)]
|
||||||
pub struct AwsRow {
|
pub struct AwsRow {
|
||||||
pub id: Uuid,
|
id: Uuid,
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
secret_key_enc: Vec<u8>,
|
secret_key_enc: Vec<u8>,
|
||||||
nonce: Vec<u8>,
|
nonce: Vec<u8>,
|
||||||
@ -53,6 +53,10 @@ impl PersistentCredential for AwsBaseCredential {
|
|||||||
|
|
||||||
fn type_name() -> &'static str { "aws" }
|
fn type_name() -> &'static str { "aws" }
|
||||||
|
|
||||||
|
fn into_credential(self) -> Credential { Credential::AwsBase(self) }
|
||||||
|
|
||||||
|
fn row_id(row: &AwsRow) -> Uuid { row.id }
|
||||||
|
|
||||||
fn from_row(row: AwsRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
|
fn from_row(row: AwsRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
|
||||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||||
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
|
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
|
||||||
@ -79,93 +83,6 @@ impl PersistentCredential for AwsBaseCredential {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// async fn save(&self, record: CredentialRecord, &Crypto, pool: &SqlitePool) -> Result<(), CredentialRecordsError> {
|
|
||||||
// let (nonce, ciphertext) = crypto.encrypt(self.secret_access_key.as_bytes())?;
|
|
||||||
// let nonce_bytes = &nonce.as_slice();
|
|
||||||
|
|
||||||
// let res = sqlx::query!(
|
|
||||||
// "INSERT INTO credentials (id, name, type, created_at)
|
|
||||||
// VALUES (?, ?, 'aws', strftime('%s'))
|
|
||||||
// ON CONFLICT(id) DO UPDATE SET
|
|
||||||
// name = excluded.name,
|
|
||||||
// type = excluded.type,
|
|
||||||
// created_at = excluded.created_at;
|
|
||||||
|
|
||||||
// INSERT OR REPLACE INTO aws_credentials (
|
|
||||||
// id,
|
|
||||||
// access_key_id,
|
|
||||||
// secret_key_enc,
|
|
||||||
// nonce
|
|
||||||
// )
|
|
||||||
// VALUES (?, ?, ?, ?);",
|
|
||||||
// id,
|
|
||||||
// name,
|
|
||||||
// id, // for the second query
|
|
||||||
// self.access_key_id,
|
|
||||||
// ciphertext,
|
|
||||||
// nonce_bytes,
|
|
||||||
// ).execute(pool).await;
|
|
||||||
|
|
||||||
// match res {
|
|
||||||
// Err(SqlxError::Database(e)) if e.code().as_deref() == Some("2067") => Err(CredentialRecordsError::Duplicate),
|
|
||||||
// Err(e) => Err(SaveCredentialsError::DbError(e)),
|
|
||||||
// Ok(_) => Ok(())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn load(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
|
||||||
// let record: AwsRecord = sqlx::query_as(
|
|
||||||
// "SELECT c.id, c.name, c.is_default, a.access_key_id, a.secret_key_enc, a.nonce
|
|
||||||
// FROM credentials c JOIN aws_credentials a ON a.id = c.id
|
|
||||||
// WHERE c.name = ?"
|
|
||||||
// ).bind(name)
|
|
||||||
// .fetch_optional(pool)
|
|
||||||
// .await?
|
|
||||||
// .ok_or(LoadCredentialsError::NoCredentials)?;
|
|
||||||
|
|
||||||
// let key = record.decrypt_key(crypto)?;
|
|
||||||
// let credential = AwsBaseCredential::new(record.access_key_id, key);
|
|
||||||
// Ok(credential)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
|
||||||
// let record: AwsRecord = sqlx::query_as(
|
|
||||||
// "SELECT c.id, c.name, c.is_default, a.access_key_id, a.secret_key_enc, a.nonce
|
|
||||||
// FROM credentials c JOIN aws_credentials a ON a.id = c.id
|
|
||||||
// WHERE c.type = 'aws' AND c.is_default = 1"
|
|
||||||
// ).fetch_optional(pool)
|
|
||||||
// .await?
|
|
||||||
// .ok_or(LoadCredentialsError::NoCredentials)?;
|
|
||||||
|
|
||||||
// let key = record.decrypt_key(crypto)?;
|
|
||||||
// let credential = AwsBaseCredential::new(record.access_key_id, key);
|
|
||||||
// Ok(credential)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<SaveCredential>, LoadCredentialsError> {
|
|
||||||
// let mut rows = sqlx::query_as::<_, AwsRecord>(
|
|
||||||
// "SELECT c.id, c.name, c.is_default, a.access_key_id, a.secret_key_enc, a.nonce
|
|
||||||
// FROM credentials c JOIN aws_credentials a ON a.id = c.id"
|
|
||||||
// ).fetch(pool);
|
|
||||||
|
|
||||||
// let mut creds = Vec::new();
|
|
||||||
|
|
||||||
// while let Some(record) = rows.try_next().await? {
|
|
||||||
// let key = record.decrypt_key(crypto)?;
|
|
||||||
// let aws = AwsBaseCredential::new(record.access_key_id, key);
|
|
||||||
|
|
||||||
// let cred = SaveCredential {
|
|
||||||
// id: record.id,
|
|
||||||
// name: record.name,
|
|
||||||
// is_default: record.is_default,
|
|
||||||
// credential: Credential::AwsBase(aws),
|
|
||||||
// };
|
|
||||||
// creds.push(cred);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(creds)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -269,6 +186,7 @@ where S: Serializer
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use sqlx::types::uuid::uuid;
|
||||||
|
|
||||||
|
|
||||||
fn creds() -> AwsBaseCredential {
|
fn creds() -> AwsBaseCredential {
|
||||||
@ -302,7 +220,8 @@ mod tests {
|
|||||||
#[sqlx::test(fixtures("aws_credentials"))]
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
async fn test_load(pool: SqlitePool) {
|
async fn test_load(pool: SqlitePool) {
|
||||||
let crypt = Crypto::fixed();
|
let crypt = Crypto::fixed();
|
||||||
let loaded = AwsBaseCredential::load(&test_uuid(), &crypt, &pool).await.unwrap();
|
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
||||||
|
let loaded = AwsBaseCredential::load(&id, &crypt, &pool).await.unwrap();
|
||||||
assert_eq!(creds(), loaded);
|
assert_eq!(creds(), loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,14 +245,14 @@ mod tests {
|
|||||||
#[sqlx::test(fixtures("aws_credentials"))]
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
async fn test_list(pool: SqlitePool) {
|
async fn test_list(pool: SqlitePool) {
|
||||||
let crypt = Crypto::fixed();
|
let crypt = Crypto::fixed();
|
||||||
let list: Vec<_> = AwsBaseCredential::list(&pool)
|
let list: Vec<_> = AwsBaseCredential::list(&crypt, &pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to load credentials")
|
.expect("Failed to load credentials")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| AwsBaseCredential::from_row(r, &crypt).unwrap())
|
.map(|(_, cred)| cred)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert_eq!(&creds(), &list[0]);
|
assert_eq!(&creds().into_credential(), &list[0]);
|
||||||
assert_eq!(&creds_2(), &list[1]);
|
assert_eq!(&creds_2().into_credential(), &list[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
use std::fmt::Formatter;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
FromRow,
|
FromRow,
|
||||||
@ -9,6 +7,7 @@ use sqlx::{
|
|||||||
Transaction,
|
Transaction,
|
||||||
types::Uuid,
|
types::Uuid,
|
||||||
};
|
};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
@ -37,7 +36,13 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
|
|||||||
type Row: Send + Unpin + for<'r> FromRow<'r, SqliteRow>;
|
type Row: Send + Unpin + for<'r> FromRow<'r, SqliteRow>;
|
||||||
|
|
||||||
fn type_name() -> &'static str;
|
fn type_name() -> &'static str;
|
||||||
|
|
||||||
|
fn into_credential(self) -> Credential;
|
||||||
|
|
||||||
|
fn row_id(row: &Self::Row) -> Uuid;
|
||||||
|
|
||||||
fn from_row(row: Self::Row, crypto: &Crypto) -> Result<Self, LoadCredentialsError>;
|
fn from_row(row: Self::Row, crypto: &Crypto) -> Result<Self, LoadCredentialsError>;
|
||||||
|
|
||||||
// save_details needs to be implemented per-type because we don't know the number of parameters in advance
|
// save_details needs to be implemented per-type because we don't know the number of parameters in advance
|
||||||
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError>;
|
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError>;
|
||||||
|
|
||||||
@ -87,9 +92,25 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
|
|||||||
Self::from_row(row, crypto)
|
Self::from_row(row, crypto)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(pool: &SqlitePool) -> Result<Vec<Self::Row>, LoadCredentialsError> {
|
async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<(Uuid, Credential)>, LoadCredentialsError> {
|
||||||
let q = format!("SELECT * FROM {}", Self::table_name());
|
let q = format!(
|
||||||
let rows: Vec<Self::Row> = sqlx::query_as(&q).fetch_all(pool).await?;
|
"SELECT details.*
|
||||||
Ok(rows)
|
FROM
|
||||||
|
{} details
|
||||||
|
JOIN credentials c
|
||||||
|
ON c.id = details.id
|
||||||
|
ORDER BY c.created_at",
|
||||||
|
Self::table_name(),
|
||||||
|
);
|
||||||
|
let mut rows = sqlx::query_as::<_, Self::Row>(&q).fetch(pool);
|
||||||
|
|
||||||
|
let mut creds = Vec::new();
|
||||||
|
while let Some(row) = rows.try_next().await? {
|
||||||
|
let id = Self::row_id(&row);
|
||||||
|
let cred = Self::from_row(row, crypto)?.into_credential();
|
||||||
|
creds.push((id, cred));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(creds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ use tokio_stream::StreamExt;
|
|||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use super::{
|
use super::{
|
||||||
AwsBaseCredential,
|
AwsBaseCredential,
|
||||||
aws::AwsRow,
|
|
||||||
Credential,
|
Credential,
|
||||||
Crypto,
|
Crypto,
|
||||||
PersistentCredential,
|
PersistentCredential,
|
||||||
@ -100,7 +99,7 @@ impl CredentialRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_details(row: CredentialRow, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
async fn load_credential(row: CredentialRow, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
let credential = match row.credential_type.as_str() {
|
let credential = match row.credential_type.as_str() {
|
||||||
"aws" => Credential::AwsBase(AwsBaseCredential::load(&row.id, crypto, pool).await?),
|
"aws" => Credential::AwsBase(AwsBaseCredential::load(&row.id, crypto, pool).await?),
|
||||||
_ => return Err(LoadCredentialsError::InvalidData),
|
_ => return Err(LoadCredentialsError::InvalidData),
|
||||||
@ -116,7 +115,7 @@ impl CredentialRecord {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
Self::load_details(row, crypto, pool).await
|
Self::load_credential(row, crypto, pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
@ -128,7 +127,7 @@ impl CredentialRecord {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
Self::load_details(row, crypto, pool).await
|
Self::load_credential(row, crypto, pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<Self>, LoadCredentialsError> {
|
pub async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<Self>, LoadCredentialsError> {
|
||||||
@ -143,19 +142,20 @@ impl CredentialRecord {
|
|||||||
|
|
||||||
let mut records = Vec::with_capacity(parent_map.len());
|
let mut records = Vec::with_capacity(parent_map.len());
|
||||||
|
|
||||||
for row in AwsBaseCredential::list(&pool).await? {
|
for (id, credential) in AwsBaseCredential::list(crypto, pool).await? {
|
||||||
let parent = parent_map.remove(&row.id)
|
let parent = parent_map.remove(&id)
|
||||||
.ok_or(LoadCredentialsError::InvalidData)?;
|
.ok_or(LoadCredentialsError::InvalidData)?;
|
||||||
let credential = Credential::AwsBase(AwsBaseCredential::from_row(row, crypto)?);
|
|
||||||
records.push(Self::from_parts(parent, credential));
|
records.push(Self::from_parts(parent, credential));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub async fn rekey(old: &Crypto, new: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
pub async fn rekey(old: &Crypto, new: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||||
todo!()
|
for record in Self::list(old, pool).await? {
|
||||||
|
record.save(new, pool).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +274,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
async fn test_overwrite_aws(pool: SqlitePool) {
|
async fn test_overwrite_aws(pool: SqlitePool) {
|
||||||
let crypt = Crypto::fixed();
|
let crypt = Crypto::fixed();
|
||||||
|
|
||||||
@ -329,6 +330,34 @@ mod tests {
|
|||||||
.expect("Failed to load other credential");
|
.expect("Failed to load other credential");
|
||||||
assert!(!other_loaded.is_default);
|
assert!(!other_loaded.is_default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_list(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
|
||||||
|
let records = CredentialRecord::list(&crypt, &pool).await
|
||||||
|
.expect("Failed to list credentials");
|
||||||
|
|
||||||
|
assert_eq!(aws_record(), records[0]);
|
||||||
|
assert_eq!(aws_record_2(), records[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_rekey(pool: SqlitePool) {
|
||||||
|
let old = Crypto::fixed();
|
||||||
|
let new = Crypto::random();
|
||||||
|
|
||||||
|
CredentialRecord::rekey(&old, &new, &pool).await
|
||||||
|
.expect("Failed to rekey credentials");
|
||||||
|
|
||||||
|
let records = CredentialRecord::list(&new, &pool).await
|
||||||
|
.expect("Failed to re-list credentials");
|
||||||
|
|
||||||
|
assert_eq!(aws_record(), records[0]);
|
||||||
|
assert_eq!(aws_record_2(), records[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,6 +79,17 @@ impl AppSession {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn reset(&mut self, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||||
|
match self {
|
||||||
|
Self::Unlocked {..} | Self::Locked {..} => {
|
||||||
|
kv::delete_multi(pool, &["salt", "verify_nonce", "verify_blob"]).await?;
|
||||||
|
*self = Self::Empty;
|
||||||
|
},
|
||||||
|
Self::Empty => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn try_get_crypto(&self) -> Result<&Crypto, GetCredentialsError> {
|
pub fn try_get_crypto(&self) -> Result<&Crypto, GetCredentialsError> {
|
||||||
match self {
|
match self {
|
||||||
Self::Empty => Err(GetCredentialsError::Empty),
|
Self::Empty => Err(GetCredentialsError::Empty),
|
||||||
|
13
src-tauri/src/fixtures/kv.sql
Normal file
13
src-tauri/src/fixtures/kv.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
INSERT INTO kv (name, value)
|
||||||
|
VALUES
|
||||||
|
-- b"hello world" (raw bytes)
|
||||||
|
('test_bytes', X'68656C6C6F20776F726C64'),
|
||||||
|
|
||||||
|
-- b"\"hello world\"" (JSON string)
|
||||||
|
('test_string', X'2268656C6C6F20776F726C6422'),
|
||||||
|
|
||||||
|
-- b"123" (JSON integer)
|
||||||
|
('test_int', X'313233'),
|
||||||
|
|
||||||
|
-- b"true" (JSON bool)
|
||||||
|
('test_bool', X'74727565')
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::types::Uuid;
|
use sqlx::types::Uuid;
|
||||||
use tauri::State;
|
use tauri::{AppHandle, State};
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::credentials::{
|
use crate::credentials::{
|
||||||
@ -80,6 +80,12 @@ pub async fn lock(app_state: State<'_, AppState>) -> Result<(), LockError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reset_session(app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
||||||
|
app_state.reset_session().await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_passphrase(passphrase: &str, app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
pub async fn set_passphrase(passphrase: &str, app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
||||||
app_state.set_passphrase(passphrase).await
|
app_state.set_passphrase(passphrase).await
|
||||||
@ -154,3 +160,9 @@ pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> {
|
|||||||
pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<String>, ()> {
|
pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<String>, ()> {
|
||||||
Ok(app_state.setup_errors.clone())
|
Ok(app_state.setup_errors.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn exit(app_handle: AppHandle) {
|
||||||
|
app_handle.exit(0)
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ use crate::errors::*;
|
|||||||
|
|
||||||
|
|
||||||
pub async fn save<T>(pool: &SqlitePool, name: &str, value: &T) -> Result<(), sqlx::Error>
|
pub async fn save<T>(pool: &SqlitePool, name: &str, value: &T) -> Result<(), sqlx::Error>
|
||||||
where T: Serialize
|
where T: Serialize + ?Sized
|
||||||
{
|
{
|
||||||
let bytes = serde_json::to_vec(value).unwrap();
|
let bytes = serde_json::to_vec(value).unwrap();
|
||||||
save_bytes(pool, name, &bytes).await
|
save_bytes(pool, name, &bytes).await
|
||||||
@ -44,9 +44,33 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> {
|
||||||
|
let placeholder = names.iter()
|
||||||
|
.map(|_| "?")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(",");
|
||||||
|
let query = format!("DELETE FROM kv WHERE name IN ({})", placeholder);
|
||||||
|
|
||||||
|
let mut q = sqlx::query(&query);
|
||||||
|
for name in names {
|
||||||
|
q = q.bind(name);
|
||||||
|
}
|
||||||
|
q.execute(pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
macro_rules! load_bytes_multi {
|
macro_rules! load_bytes_multi {
|
||||||
(
|
(
|
||||||
$pool:ident,
|
$pool:expr,
|
||||||
$($name:literal),*
|
$($name:literal),*
|
||||||
) => {
|
) => {
|
||||||
// wrap everything up in an async block for easy short-circuiting...
|
// wrap everything up in an async block for easy short-circuiting...
|
||||||
@ -78,7 +102,7 @@ pub(crate) use load_bytes_multi;
|
|||||||
|
|
||||||
// macro_rules! load_multi {
|
// macro_rules! load_multi {
|
||||||
// (
|
// (
|
||||||
// $pool:ident,
|
// $pool:expr,
|
||||||
// $($name:literal),*
|
// $($name:literal),*
|
||||||
// ) => {
|
// ) => {
|
||||||
// (|| {
|
// (|| {
|
||||||
@ -93,3 +117,94 @@ pub(crate) use load_bytes_multi;
|
|||||||
// })()
|
// })()
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_save_bytes(pool: SqlitePool) {
|
||||||
|
save_bytes(&pool, "test_bytes", b"hello world").await
|
||||||
|
.expect("Failed to save bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_save(pool: SqlitePool) {
|
||||||
|
save(&pool, "test_string", "hello world").await
|
||||||
|
.expect("Failed to save string");
|
||||||
|
save(&pool, "test_int", &123).await
|
||||||
|
.expect("Failed to save integer");
|
||||||
|
save(&pool, "test_bool", &true).await
|
||||||
|
.expect("Failed to save bool");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load_bytes(pool: SqlitePool) {
|
||||||
|
let bytes = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load bytes")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
|
||||||
|
assert_eq!(bytes, Vec::from(b"hello world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load(pool: SqlitePool) {
|
||||||
|
let string: String = load(&pool, "test_string").await
|
||||||
|
.expect("Failed to load string")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(string, "hello world".to_string());
|
||||||
|
|
||||||
|
let integer: usize = load(&pool, "test_int").await
|
||||||
|
.expect("Failed to load integer")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(integer, 123);
|
||||||
|
|
||||||
|
let boolean: bool = load(&pool, "test_bool").await
|
||||||
|
.expect("Failed to load boolean")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(boolean, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load_multi(pool: SqlitePool) {
|
||||||
|
let (bytes, boolean) = load_bytes_multi!(&pool, "test_bytes", "test_bool")
|
||||||
|
.await
|
||||||
|
.expect("Failed to load items")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
|
||||||
|
assert_eq!(bytes, Vec::from(b"hello world"));
|
||||||
|
assert_eq!(boolean, Vec::from(b"true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_delete(pool: SqlitePool) {
|
||||||
|
delete(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to delete data");
|
||||||
|
|
||||||
|
let loaded = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load data");
|
||||||
|
assert_eq!(loaded, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_delete_multi(pool: SqlitePool) {
|
||||||
|
delete_multi(&pool, &["test_bytes", "test_string"]).await
|
||||||
|
.expect("Failed to delete keys");
|
||||||
|
|
||||||
|
let bytes_opt = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load bytes");
|
||||||
|
assert_eq!(bytes_opt, None);
|
||||||
|
|
||||||
|
let string_opt = load_bytes(&pool, "test_string").await
|
||||||
|
.expect("Failed to load string");
|
||||||
|
assert_eq!(string_opt, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -254,6 +254,13 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn reset_session(&self) -> Result<(), SaveCredentialsError> {
|
||||||
|
let mut session = self.app_session.write().await;
|
||||||
|
session.reset(&self.pool).await?;
|
||||||
|
sqlx::query!("DELETE FROM credentials").execute(&self.pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_aws_base(&self, name: &str) -> Result<AwsBaseCredential, GetCredentialsError> {
|
pub async fn get_aws_base(&self, name: &str) -> Result<AwsBaseCredential, 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()?;
|
||||||
|
@ -7,11 +7,34 @@
|
|||||||
export let slideDuration = 150;
|
export let slideDuration = 150;
|
||||||
let animationClass = "";
|
let animationClass = "";
|
||||||
|
|
||||||
export function shake() {
|
let error = null;
|
||||||
|
|
||||||
|
function shake() {
|
||||||
animationClass = 'shake';
|
animationClass = 'shake';
|
||||||
window.setTimeout(() => animationClass = "", 400);
|
window.setTimeout(() => animationClass = "", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function run(fallible) {
|
||||||
|
try {
|
||||||
|
const ret = await Promise.resolve(fallible());
|
||||||
|
error = null;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (error) shake();
|
||||||
|
error = e;
|
||||||
|
// re-throw so it can be caught by the caller if necessary
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a method rather than a prop so that we can re-shake every time
|
||||||
|
// the error occurs, even if the error message doesn't change
|
||||||
|
export function setError(e) {
|
||||||
|
if (error) shake();
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -51,15 +74,17 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<div in:slide="{{duration: slideDuration}}" class="alert alert-error shadow-lg {animationClass} {extraClasses}">
|
{#if error}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
<div transition:slide="{{duration: slideDuration}}" class="alert alert-error shadow-lg {animationClass} {extraClasses}">
|
||||||
<span>
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
<slot></slot>
|
<span>
|
||||||
</span>
|
<slot {error}>{error.msg || error}</slot>
|
||||||
|
</span>
|
||||||
|
|
||||||
{#if $$slots.buttons}
|
{#if $$slots.buttons}
|
||||||
<div>
|
<div>
|
||||||
<slot name="buttons"></slot>
|
<slot name="buttons"></slot>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
export let value = '';
|
export let value = '';
|
||||||
export let placeholder = '';
|
export let placeholder = '';
|
||||||
export let autofocus = false;
|
export let autofocus = false;
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class};
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
</script>
|
</script>
|
||||||
@ -21,9 +23,9 @@
|
|||||||
<input
|
<input
|
||||||
type={show ? 'text' : 'password'}
|
type={show ? 'text' : 'password'}
|
||||||
{value} {placeholder} {autofocus}
|
{value} {placeholder} {autofocus}
|
||||||
on:input on:change on:focus on:blur
|
|
||||||
class="input input-bordered flex-grow join-item placeholder:text-gray-500"
|
|
||||||
on:input={e => value = e.target.value}
|
on:input={e => value = e.target.value}
|
||||||
|
on:input on:change on:focus on:blur
|
||||||
|
class="input input-bordered flex-grow join-item placeholder:text-gray-500 {classes}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
8
src/ui/icons/arrow-right-start-on-rectangle.svelte
Normal file
8
src/ui/icons/arrow-right-start-on-rectangle.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
|
||||||
|
</svg>
|
8
src/ui/icons/command-line.svelte
Normal file
8
src/ui/icons/command-line.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||||
|
</svg>
|
8
src/ui/icons/key.svelte
Normal file
8
src/ui/icons/key.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
|
||||||
|
</svg>
|
8
src/ui/icons/shield-check.svelte
Normal file
8
src/ui/icons/shield-check.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
|
||||||
|
</svg>
|
@ -1,13 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import ErrorAlert from '../ErrorAlert.svelte';
|
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-wrap justify-between gap-y-4">
|
<div class="flex flex-wrap justify-between gap-4">
|
||||||
<h3 class="text-lg font-bold shrink-0">{title}</h3>
|
<h3 class="text-lg font-bold shrink-0">{title}</h3>
|
||||||
{#if $$slots.input}
|
{#if $$slots.input}
|
||||||
<slot name="input"></slot>
|
<slot name="input"></slot>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
// 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 + 50);
|
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 50);
|
||||||
|
|
||||||
let error, alert;
|
let alert;
|
||||||
let success = false;
|
let success = false;
|
||||||
async function sendResponse() {
|
async function sendResponse() {
|
||||||
try {
|
try {
|
||||||
@ -20,14 +20,14 @@
|
|||||||
window.setTimeout(cleanupRequest, rehideDelay);
|
window.setTimeout(cleanupRequest, rehideDelay);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (error) {
|
// reset to null so that we go back to asking for approval
|
||||||
alert.shake();
|
$appState.currentRequest.response = null;
|
||||||
}
|
// setTimeout forces this to not happen until the alert has been rendered
|
||||||
error = e;
|
window.setTimeout(() => alert.setError(e), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleResponse() {
|
async function handleResponseCollected() {
|
||||||
if (
|
if (
|
||||||
$appState.sessionStatus === 'unlocked'
|
$appState.sessionStatus === 'unlocked'
|
||||||
|| $appState.currentRequest.response.approval === 'Denied'
|
|| $appState.currentRequest.response.approval === 'Denied'
|
||||||
@ -41,20 +41,17 @@
|
|||||||
{#if success}
|
{#if success}
|
||||||
<!-- if we have successfully sent a response, show it -->
|
<!-- if we have successfully sent a response, show it -->
|
||||||
<ShowResponse />
|
<ShowResponse />
|
||||||
{:else if !$appState.currentRequest?.response || error}
|
{:else if !$appState.currentRequest?.response}
|
||||||
<!-- if there's no response, or if there was an error sending it, ask for response -->
|
<!-- if a response hasn't been collected, ask for it -->
|
||||||
<div class="flex flex-col space-y-4 p-4 m-auto max-w-xl h-screen items-center justify-center">
|
<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}>
|
||||||
<ErrorAlert bind:this={alert}>
|
<svelte:fragment slot="buttons">
|
||||||
{error.msg}
|
<button class="btn btn-sm btn-alert-error" on:click={cleanupRequest}>Cancel</button>
|
||||||
<svelte:fragment slot="buttons">
|
<button class="btn btn-sm btn-alert-error" on:click={sendResponse}>Retry</button>
|
||||||
<button class="btn btn-sm btn-alert-error" on:click={cleanupRequest}>Cancel</button>
|
</svelte:fragment>
|
||||||
<button class="btn btn-sm btn-alert-error" on:click={sendResponse}>Retry</button>
|
</ErrorAlert>
|
||||||
</svelte:fragment>
|
|
||||||
</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CollectResponse on:response={handleResponse} />
|
<CollectResponse on:response={handleResponseCollected} />
|
||||||
</div>
|
</div>
|
||||||
{:else if $appState.sessionStatus === 'locked'}
|
{:else if $appState.sessionStatus === 'locked'}
|
||||||
<!-- if session is locked and we do have a response, we must be waiting for unlock -->
|
<!-- if session is locked and we do have a response, we must be waiting for unlock -->
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
|
||||||
import { emit } from '@tauri-apps/api/event';
|
|
||||||
import { getRootCause } from '../lib/errors.js';
|
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
|
||||||
import { navigate } from '../lib/routing.js';
|
|
||||||
import Link from '../ui/Link.svelte';
|
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
|
||||||
import Spinner from '../ui/Spinner.svelte';
|
|
||||||
|
|
||||||
|
|
||||||
let errorMsg = null;
|
|
||||||
let alert;
|
|
||||||
let AccessKeyId, SecretAccessKey, passphrase, confirmPassphrase
|
|
||||||
|
|
||||||
function confirm() {
|
|
||||||
if (passphrase !== confirmPassphrase) {
|
|
||||||
errorMsg = 'Passphrases do not match.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let saving = false;
|
|
||||||
async function save() {
|
|
||||||
if (passphrase !== confirmPassphrase) {
|
|
||||||
alert.shake();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let credentials = {AccessKeyId, SecretAccessKey};
|
|
||||||
try {
|
|
||||||
saving = true;
|
|
||||||
await invoke('save_credentials', {credentials, passphrase});
|
|
||||||
emit('credentials-event', 'entered');
|
|
||||||
if ($appState.currentRequest) {
|
|
||||||
navigate('Approve');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
navigate('Home');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
const root = getRootCause(e);
|
|
||||||
if (e.code === 'GetSession' && root.code) {
|
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// some of the built-in Tauri errors are plain strings,
|
|
||||||
// so fall back to e if e.msg doesn't exist
|
|
||||||
errorMsg = e.msg || e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the alert already existed, shake it
|
|
||||||
if (alert) {
|
|
||||||
alert.shake();
|
|
||||||
}
|
|
||||||
|
|
||||||
saving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
emit('credentials-event', 'enter-canceled');
|
|
||||||
navigate('Home');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<form action="#" on:submit|preventDefault="{save}" class="form-control space-y-4 max-w-sm m-auto p-4 h-screen justify-center">
|
|
||||||
<h2 class="text-2xl font-bold text-center">Enter your credentials</h2>
|
|
||||||
|
|
||||||
{#if errorMsg}
|
|
||||||
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<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="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} />
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
{#if saving }
|
|
||||||
<Spinner class="w-5 h-5" thickness="12"/>
|
|
||||||
{:else}
|
|
||||||
Submit
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<Link target={cancel} hotkey="Escape">
|
|
||||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
|
||||||
</Link>
|
|
||||||
</form>
|
|
@ -31,23 +31,37 @@
|
|||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="grid grid-cols-2 gap-6">
|
||||||
<h2 class="text-2xl font-bold">Waiting for requests</h2>
|
<Link target="ManageCredentials">
|
||||||
<button class="btn btn-primary w-full" on:click={launchTerminal}>
|
<div class="flex flex-col items-center gap-4 h-full max-w-56 rounded-box p-4 border border-primary hover:bg-base-200 transition-colors">
|
||||||
Launch Terminal
|
<Icon name="key" class="size-12 stroke-1 stroke-primary" />
|
||||||
</button>
|
<h3 class="text-lg font-bold">Credentials</h3>
|
||||||
<label class="label cursor-pointer flex items-center space-x-2">
|
<p class="text-sm">Add, remove, and change defaults credentials.</p>
|
||||||
<span class="label-text">Launch with long-lived credentials</span>
|
</div>
|
||||||
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={launchBase}>
|
|
||||||
</label>
|
|
||||||
<button class="btn btn-primary w-full" on:click={lock}>
|
|
||||||
Lock Creddy
|
|
||||||
</button>
|
|
||||||
<Link target="ChangePassphrase" class="w-full">
|
|
||||||
<button class="btn btn-primary w-full">Change passphrase</button>
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link target="ManageCredentials" class="w-full">
|
|
||||||
<button class="btn btn-primary w-full">Manage credentials</button>
|
<Link target={launchTerminal}>
|
||||||
|
<div class="flex flex-col items-center gap-4 h-full max-w-56 rounded-box p-4 border border-secondary hover:bg-base-200 transition-colors">
|
||||||
|
<Icon name="command-line" class="size-12 stroke-1 stroke-secondary" />
|
||||||
|
<h3 class="text-lg font-bold">Terminal</h3>
|
||||||
|
<p class="text-sm">Launch a terminal pre-configured with AWS credentials.</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link target={lock}>
|
||||||
|
<div class="flex flex-col items-center gap-4 h-full max-w-56 rounded-box p-4 border border-accent hover:bg-base-200 transition-colors">
|
||||||
|
<Icon name="shield-check" class="size-12 stroke-1 stroke-accent" />
|
||||||
|
<h3 class="text-lg font-bold">Lock</h3>
|
||||||
|
<p class="text-sm">Lock Creddy.</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link target={() => invoke('exit')}>
|
||||||
|
<div class="flex flex-col items-center gap-4 h-full max-w-56 rounded-box p-4 border border-warning hover:bg-base-200 transition-colors">
|
||||||
|
<Icon name="arrow-right-start-on-rectangle" class="size-12 stroke-1 stroke-warning" />
|
||||||
|
<h3 class="text-lg font-bold">Exit</h3>
|
||||||
|
<p class="text-sm">Close Creddy.</p>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { slide, fade } from 'svelte/transition';
|
import { slide, fade } from 'svelte/transition';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
import AwsCredential from './credentials/AwsCredential.svelte';
|
import AwsCredential from './credentials/AwsCredential.svelte';
|
||||||
@ -10,19 +11,20 @@
|
|||||||
let show = false;
|
let show = false;
|
||||||
|
|
||||||
let records = []
|
let records = []
|
||||||
|
let defaults = writable({});
|
||||||
async function loadCreds() {
|
async function loadCreds() {
|
||||||
records = await invoke('list_credentials');
|
records = await invoke('list_credentials');
|
||||||
console.log(records);
|
let pairs = records.filter(r => r.is_default).map(r => [r.credential.type, r.id]);
|
||||||
|
$defaults = Object.fromEntries(pairs);
|
||||||
}
|
}
|
||||||
onMount(loadCreds);
|
onMount(loadCreds);
|
||||||
|
|
||||||
function newCred() {
|
function newAws() {
|
||||||
console.log('hello!');
|
|
||||||
records.push({
|
records.push({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: '',
|
name: null,
|
||||||
is_default: false,
|
is_default: false,
|
||||||
credential: {type: 'AwsBase', AccessKeyId: '', SecretAccessKey: ''},
|
credential: {type: 'AwsBase', AccessKeyId: null, SecretAccessKey: null},
|
||||||
isNew: true,
|
isNew: true,
|
||||||
});
|
});
|
||||||
records = records;
|
records = records;
|
||||||
@ -34,26 +36,24 @@
|
|||||||
<h1 slot="title" class="text-2xl font-bold">Credentials</h1>
|
<h1 slot="title" class="text-2xl font-bold">Credentials</h1>
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
<div class="max-w-xl mx-auto flex flex-col gap-y-4 justify-center">
|
<div class="max-w-xl mx-auto mb-12 flex flex-col gap-y-4 justify-center">
|
||||||
<div class="divider">
|
<div class="divider">
|
||||||
<h2 class="text-xl font-bold">AWS Access Keys</h2>
|
<h2 class="text-xl font-bold">AWS Access Keys</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if records.length > 0}
|
{#if records.length > 0}
|
||||||
<div class="rounded-box border-2 border-neutral-content/30 divide-y-2 divide-neutral-content/30">
|
{#each records as record (record.id)}
|
||||||
{#each records as record (record.id)}
|
<AwsCredential {record} {defaults} on:update={loadCreds} />
|
||||||
<AwsCredential {record} on:update={loadCreds} />
|
{/each}
|
||||||
{/each}
|
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newCred}>
|
|
||||||
<Icon name="plus-circle-mini" class="size-5" />
|
<Icon name="plus-circle-mini" class="size-5" />
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
|
<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 AWS credentials.</div>
|
<div>You have no saved AWS credentials.</div>
|
||||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newCred}>
|
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||||
<Icon name="plus-circle-mini" />
|
<Icon name="plus-circle-mini" class="size-5" />
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
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 SettingsGroup from '../ui/settings/SettingsGroup.svelte';
|
import SettingsGroup from '../ui/settings/SettingsGroup.svelte';
|
||||||
import Keybind from '../ui/settings/Keybind.svelte';
|
import Keybind from '../ui/settings/Keybind.svelte';
|
||||||
import { Setting, ToggleSetting, NumericSetting, FileSetting, TextSetting, TimeSetting } from '../ui/settings';
|
import { Setting, ToggleSetting, NumericSetting, FileSetting, TextSetting, TimeSetting } from '../ui/settings';
|
||||||
@ -21,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');
|
||||||
}
|
}
|
||||||
@ -75,12 +75,12 @@
|
|||||||
</TimeSetting>
|
</TimeSetting>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Setting title="Update credentials">
|
<Setting title="Update passphrase">
|
||||||
<Link slot="input" target="EnterAwsCredential">
|
<Link slot="input" target="ChangePassphrase">
|
||||||
<button type="button" class="btn btn-sm btn-primary">Update</button>
|
<button type="button" class="btn btn-sm btn-primary">Update</button>
|
||||||
</Link>
|
</Link>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Update or re-enter your encrypted credentials.
|
Change your master passphrase.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Setting>
|
</Setting>
|
||||||
|
|
||||||
|
@ -10,40 +10,29 @@
|
|||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
import PassphraseInput from '../ui/PassphraseInput.svelte';
|
import PassphraseInput from '../ui/PassphraseInput.svelte';
|
||||||
|
import ResetPassphrase from './passphrase/ResetPassphrase.svelte';
|
||||||
import Spinner from '../ui/Spinner.svelte';
|
import Spinner from '../ui/Spinner.svelte';
|
||||||
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
||||||
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let errorMsg = null;
|
|
||||||
let alert;
|
let alert;
|
||||||
let passphrase = '';
|
let passphrase = '';
|
||||||
|
|
||||||
let saving = false;
|
let saving = false;
|
||||||
async function unlock() {
|
async function unlock() {
|
||||||
|
saving = true;
|
||||||
try {
|
try {
|
||||||
saving = true;
|
await alert.run(async () => invoke('unlock', {passphrase}));
|
||||||
let r = await invoke('unlock', {passphrase});
|
|
||||||
$appState.sessionStatus = 'unlocked';
|
$appState.sessionStatus = 'unlocked';
|
||||||
emit('unlocked');
|
emit('unlocked');
|
||||||
dispatch('unlocked');
|
dispatch('unlocked');
|
||||||
}
|
}
|
||||||
catch (e) {
|
finally {
|
||||||
const root = getRootCause(e);
|
|
||||||
if (e.code === 'GetSession' && root.code) {
|
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
errorMsg = e.msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the alert already existed, shake it
|
|
||||||
if (alert) {
|
|
||||||
alert.shake();
|
|
||||||
}
|
|
||||||
|
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -60,9 +49,7 @@
|
|||||||
<label class="space-y-4">
|
<label class="space-y-4">
|
||||||
<h2 class="font-bold text-xl text-center">Please enter your passphrase</h2>
|
<h2 class="font-bold text-xl text-center">Please enter your passphrase</h2>
|
||||||
|
|
||||||
{#if errorMsg}
|
<ErrorAlert bind:this="{alert}" />
|
||||||
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
<PassphraseInput autofocus="true" bind:value={passphrase} placeholder="correct horse battery staple" />
|
<PassphraseInput autofocus="true" bind:value={passphrase} placeholder="correct horse battery staple" />
|
||||||
@ -75,4 +62,6 @@
|
|||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<ResetPassphrase />
|
||||||
</form>
|
</form>
|
||||||
|
@ -6,39 +6,36 @@
|
|||||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
import Icon from '../../ui/Icon.svelte';
|
import Icon from '../../ui/Icon.svelte';
|
||||||
|
|
||||||
export let record
|
export let record;
|
||||||
|
export let defaults;
|
||||||
|
|
||||||
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||||
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
// if record.credential is blank when component is first instantiated, this is
|
|
||||||
// a newly-added credential, so show details so that data can be filled out
|
|
||||||
let showDetails = record.isNew ? true : false;
|
let showDetails = record.isNew ? true : false;
|
||||||
|
|
||||||
let localName = name;
|
let localName = name;
|
||||||
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);
|
||||||
|
|
||||||
let error, alert;
|
// explicitly subscribe to updates to `default`, so that we can update
|
||||||
|
// our local copy even if the component hasn't been recreated
|
||||||
|
// (sadly we can't use a reactive binding because reasons I guess)
|
||||||
|
defaults.subscribe(d => local.is_default = local.id === d[local.credential.type])
|
||||||
|
|
||||||
|
let alert;
|
||||||
async function saveCredential() {
|
async function saveCredential() {
|
||||||
try {
|
await invoke('save_credential', {record: local});
|
||||||
await invoke('save_credential', {cred: local});
|
dispatch('update');
|
||||||
dispatch('update');
|
showDetails = false;
|
||||||
showDetails = false;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if (error) alert.shake();
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deleteModal;
|
||||||
let confirmDelete;
|
|
||||||
function conditionalDelete() {
|
function conditionalDelete() {
|
||||||
if (!record.isNew) {
|
if (!record.isNew) {
|
||||||
confirmDelete.showModal();
|
deleteModal.showModal();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
deleteCredential();
|
deleteCredential();
|
||||||
@ -48,62 +45,63 @@
|
|||||||
async function deleteCredential() {
|
async function deleteCredential() {
|
||||||
try {
|
try {
|
||||||
if (!record.isNew) {
|
if (!record.isNew) {
|
||||||
|
|
||||||
await invoke('delete_credential', {id: record.id});
|
await invoke('delete_credential', {id: record.id});
|
||||||
}
|
}
|
||||||
dispatch('update');
|
dispatch('update');
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (error) alert.shake();
|
showDetails = true;
|
||||||
error = e;
|
// wait for showDetails to take effect and the alert to be rendered
|
||||||
|
window.setTimeout(() => alert.setError(e), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div transition:slide|local={{duration: record.isNew ? 300 : 0}} class="px-6 py-4 space-y-4">
|
<div
|
||||||
<div class="flex items-center gap-x-4">
|
transition:slide|local={{duration: record.isNew ? 300 : 0}}
|
||||||
<h3 class="text-lg font-bold">{record.name}</h3>
|
class="rounded-box space-y-4 bg-base-200 {record.is_default ? 'border border-accent' : ''}"
|
||||||
|
>
|
||||||
|
<div class="flex items-center px-6 py-4 gap-x-4">
|
||||||
|
<h3 class="text-lg font-bold">{record.name || ''}</h3>
|
||||||
|
|
||||||
{#if record.is_default}
|
{#if record.is_default}
|
||||||
<span class="badge badge-secondary">Default</span>
|
<span class="badge badge-accent">Default</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="join ml-auto">
|
<div class="join ml-auto">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-primary join-item"
|
class="btn btn-outline join-item"
|
||||||
on:click={() => showDetails = !showDetails}
|
on:click={() => showDetails = !showDetails}
|
||||||
>
|
>
|
||||||
<Icon name="pencil" class="w-5 h-5" />
|
<Icon name="pencil" class="size-6" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-error join-item"
|
class="btn btn-outline btn-error join-item"
|
||||||
on:click={conditionalDelete}
|
on:click={conditionalDelete}
|
||||||
>
|
>
|
||||||
<Icon name="trash" class="w-5 h-5" />
|
<Icon name="trash" class="size-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{#if showDetails}
|
{#if showDetails}
|
||||||
{#if error}
|
|
||||||
<ErrorAlert bind:this={alert}>{error}</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<form
|
<form
|
||||||
transition:slide|local={{duration: 200}}
|
transition:slide|local={{duration: 200}}
|
||||||
class="space-y-4"
|
class=" px-6 pb-4 space-y-4"
|
||||||
on:submit|preventDefault={saveCredential}
|
on:submit|preventDefault={() => alert.run(saveCredential)}
|
||||||
>
|
>
|
||||||
|
<ErrorAlert bind:this={alert} />
|
||||||
|
|
||||||
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
||||||
{#if record.isNew}
|
{#if record.isNew}
|
||||||
<span class="justify-self-end">Name</span>
|
<span class="justify-self-end">Name</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered"
|
class="input input-bordered bg-transparent"
|
||||||
bind:value={local.name}
|
bind:value={local.name}
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
@ -111,20 +109,20 @@
|
|||||||
<span class="justify-self-end">Key ID</span>
|
<span class="justify-self-end">Key ID</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered font-mono"
|
class="input input-bordered font-mono bg-transparent"
|
||||||
bind:value={local.credential.AccessKeyId}
|
bind:value={local.credential.AccessKeyId}
|
||||||
>
|
>
|
||||||
|
|
||||||
<span>Secret key</span>
|
<span>Secret key</span>
|
||||||
<div class="font-mono">
|
<div class="font-mono">
|
||||||
<PassphraseInput bind:value={local.credential.SecretAccessKey} />
|
<PassphraseInput class="bg-transparent" bind:value={local.credential.SecretAccessKey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<label class="label cursor-pointer justify-self-start space-x-4">
|
<label class="label cursor-pointer justify-self-start space-x-4">
|
||||||
<span class="label-text">Default for type</span>
|
<span class="label-text">Default AWS access key</span>
|
||||||
<input type="checkbox" class="toggle toggle-secondary" bind:checked={local.is_default}>
|
<input type="checkbox" class="toggle toggle-accent" bind:checked={local.is_default}>
|
||||||
</label>
|
</label>
|
||||||
{#if isModified}
|
{#if isModified}
|
||||||
<button
|
<button
|
||||||
@ -139,7 +137,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<dialog bind:this={confirmDelete} class="modal">
|
<dialog bind:this={deleteModal} class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3 class="text-lg font-bold">Delete AWS credential "{record.name}"?</h3>
|
<h3 class="text-lg font-bold">Delete AWS credential "{record.name}"?</h3>
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
import Link from '../../ui/Link.svelte';
|
import Link from '../../ui/Link.svelte';
|
||||||
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||||
|
import ResetPassphrase from './ResetPassphrase.svelte';
|
||||||
import Spinner from '../../ui/Spinner.svelte';
|
import Spinner from '../../ui/Spinner.svelte';
|
||||||
|
|
||||||
export let cancellable = false;
|
export let cancellable = false;
|
||||||
@ -13,48 +14,68 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let alert, saving;
|
let alert;
|
||||||
|
let saving = false;
|
||||||
let passphrase = '';
|
let passphrase = '';
|
||||||
let confirmPassphrase = '';
|
let confirmPassphrase = '';
|
||||||
let error = null;
|
|
||||||
|
|
||||||
function confirm() {
|
// onChange only fires when an input loses focus, so always set the error if not set
|
||||||
|
function onChange() {
|
||||||
|
console.log(`onChange: passphrase=${passphrase}, confirmPassphrase=${confirmPassphrase}`)
|
||||||
if (passphrase !== confirmPassphrase) {
|
if (passphrase !== confirmPassphrase) {
|
||||||
error = 'Passphrases do not match.';
|
alert.setError('Passphrases do not match.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alert.setError(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// onInput fires on every keystroke, so only dismiss the error, don't create it
|
||||||
|
function onInput() {
|
||||||
|
console.log(`onInput: passphrase=${passphrase}, confirmPassphrase=${confirmPassphrase}`)
|
||||||
|
if (passphrase === confirmPassphrase) {
|
||||||
|
alert.setError(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (passphrase === '' || passphrase !== confirmPassphrase) {
|
if (passphrase !== confirmPassphrase) {
|
||||||
alert.shake();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passphrase === '') {
|
||||||
|
alert.setError('Passphrase is empty.')
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
await invoke('set_passphrase', {passphrase});
|
await alert.run(async () => {
|
||||||
$appState.sessionStatus = 'unlocked';
|
await invoke('set_passphrase', {passphrase})
|
||||||
dispatch('save');
|
throw('something bad happened');
|
||||||
|
$appState.sessionStatus = 'unlocked';
|
||||||
|
dispatch('save');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (e) {
|
finally {
|
||||||
if (error) alert.shake();
|
saving = false;
|
||||||
error = e;
|
|
||||||
}
|
}
|
||||||
saving = false;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<form class="form-control gap-y-4" on:submit|preventDefault={save}>
|
<form class="form-control gap-y-4" on:submit|preventDefault={save}>
|
||||||
{#if error}
|
<ErrorAlert bind:this={alert} />
|
||||||
<ErrorAlert bind:this={alert}>{error}</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<label class="form-control w-full">
|
<label class="form-control w-full">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Passphrase</span>
|
<span class="label-text">Passphrase</span>
|
||||||
</div>
|
</div>
|
||||||
<PassphraseInput bind:value={passphrase} placeholder="correct horse battery staple" />
|
<PassphraseInput
|
||||||
|
bind:value={passphrase}
|
||||||
|
on:input={onInput}
|
||||||
|
placeholder="correct horse battery staple"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="form-control w-full">
|
<label class="form-control w-full">
|
||||||
@ -63,8 +84,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<PassphraseInput
|
<PassphraseInput
|
||||||
bind:value={confirmPassphrase}
|
bind:value={confirmPassphrase}
|
||||||
|
on:input={onInput} on:change={onChange}
|
||||||
placeholder="correct horse battery staple"
|
placeholder="correct horse battery staple"
|
||||||
on:change={confirm}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@ -77,8 +98,12 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if cancellable}
|
{#if cancellable}
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target="Settings" hotkey="Escape">
|
||||||
<button type="button" class="btn btn-outline btn-sm w-full">Cancel</button>
|
<button type="button" class="btn btn-outline btn-sm w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $appState.sessionStatus === 'locked'}
|
||||||
|
<ResetPassphrase />
|
||||||
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
41
src/views/passphrase/ResetPassphrase.svelte
Normal file
41
src/views/passphrase/ResetPassphrase.svelte
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script>
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { appState } from '../../lib/state.js';
|
||||||
|
|
||||||
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
let modal;
|
||||||
|
let alert;
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
await invoke('reset_session');
|
||||||
|
$appState.sessionStatus = 'empty';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="button" class="self-end text-sm text-secondary/75 hover:underline focus:ring-accent" on:click={modal.showModal()}>
|
||||||
|
Reset passphrase
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog class="modal" bind:this={modal}>
|
||||||
|
<div class="modal-box space-y-6">
|
||||||
|
<ErrorAlert bind:this={alert} />
|
||||||
|
<h3 class="text-lg font-bold">Delete all credentials?</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>Credentials are encrypted with your current passphrase and will be lost if the passphrase is reset.</p>
|
||||||
|
<p>Are you sure you want to reset your passphrase and delete all saved credentials?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog" class="flex gap-x-4">
|
||||||
|
<button autofocus class="btn btn-outline">Cancel</button>
|
||||||
|
<button class="btn btn-error" on:click|preventDefault={() => alert.run(reset)}>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
@ -14,18 +14,35 @@ module.exports = {
|
|||||||
themes: [
|
themes: [
|
||||||
{
|
{
|
||||||
creddy: {
|
creddy: {
|
||||||
"primary": "#115e59",
|
"primary": "#0ea5e9",
|
||||||
"secondary": "#8b5cf6",
|
"secondary": "#fb923c",
|
||||||
"accent": "#0ea5e9",
|
"accent": "#8b5cf6",
|
||||||
"neutral": "#2f292c",
|
"neutral": "#2f292c",
|
||||||
"base-100": "#252e3a",
|
"base-100": "#252e3a",
|
||||||
"info": "#3aa8ff",
|
"info": "#66cccc",
|
||||||
"success": "#52bf73",
|
"success": "#52bf73",
|
||||||
"warning": "#d97706",
|
"warning": "#d1a900",
|
||||||
"error": "#f87171",
|
"error": "#f87171",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"summer-night": {
|
||||||
|
"primary": "#0ea5e9",
|
||||||
|
"secondary": "#0ea5e9",
|
||||||
|
"accent": "#fb923c",
|
||||||
|
"neutral": "#393939",
|
||||||
|
"base-100": "#2d2d2d",
|
||||||
|
"info": "#66cccc",
|
||||||
|
"success": "#22c55e",
|
||||||
|
"warning": "#d1a900",
|
||||||
|
"error": "#f2777a"
|
||||||
|
},
|
||||||
|
},
|
||||||
"dark",
|
"dark",
|
||||||
|
"night",
|
||||||
|
"dracula",
|
||||||
|
"sunset",
|
||||||
|
"dim",
|
||||||
"light"
|
"light"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user