mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
remove product key from setup flow (#1750)
* remove product key flow from setup * feat: backend turned off encryption + new Id + no package id * implement new encryption scheme in FE * decode response string * crypto not working * update setup wizard closes #1762 * feat: Get the encryption key * fix: Get to recovery * remove old code * fix build * fix: Install works for now * fix bug in config for adding new list items * dismiss action modal on success * clear button in config * wip: Currently broken in avahi mdns * include headers with req/res and refactor patchDB init and usage * fix: Can now run in the main * flatline on failed init * update patch DB * add last-wifi-region to data model even though not used by FE * chore: Fix the start. * wip: Fix wrong order for getting hostname before sql has been created * fix edge case where union keys displayed as new when not new * fix: Can start * last backup color, markdown links always new tab, fix bug with login * refactor to remove WithRevision * resolve circular dep issue * update submodule * fix patch-db * update patchDB * update patch again * escape error * decodeuricomponent * increase proxy buffer size * increase proxy buffer size * fix nginx Co-authored-by: BluJ <mogulslayer@gmail.com> Co-authored-by: BluJ <dragondef@gmail.com> Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
669
backend/Cargo.lock
generated
669
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,7 @@ imbl = "2.0.0"
|
||||
indexmap = { version = "1.9.1", features = ["serde"] }
|
||||
isocountry = "0.3.2"
|
||||
itertools = "0.10.3"
|
||||
josekit = "0.8.1"
|
||||
js_engine = { path = '../libs/js_engine', optional = true }
|
||||
jsonpath_lib = "0.3.0"
|
||||
lazy_static = "1.4.0"
|
||||
@@ -141,6 +142,7 @@ tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
|
||||
trust-dns-server = "0.21.2"
|
||||
typed-builder = "0.10.0"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
|
||||
[profile.test]
|
||||
opt-level = 3
|
||||
|
||||
@@ -6,7 +6,8 @@ Wants=avahi-daemon.service nginx.service tor.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=RUST_LOG=embassy_init=debug,embassy=debug,js_engine=debug
|
||||
Environment=RUST_LOG=embassy_init=debug,embassy=debug,js_engine=debug,patch_db=trace
|
||||
Environment=RUST_LIB_BACKTRACE=full
|
||||
ExecStart=/usr/local/bin/embassy-init
|
||||
RemainAfterExit=true
|
||||
StandardOutput=file:/var/log/embassy-init.out.log
|
||||
|
||||
1296
backend/src/assets/adjectives.txt
Normal file
1296
backend/src/assets/adjectives.txt
Normal file
File diff suppressed because it is too large
Load Diff
7776
backend/src/assets/nouns.txt
Normal file
7776
backend/src/assets/nouns.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,6 @@ use crate::auth::check_password_against_db;
|
||||
use crate::backup::{BackupReport, ServerBackupReport};
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::BackupProgress;
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::disk::mount::backup::BackupMountGuard;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
@@ -135,7 +134,7 @@ pub async fn backup_all(
|
||||
)]
|
||||
package_ids: Option<BTreeSet<PackageId>>,
|
||||
#[arg] password: String,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
check_password_against_db(&mut ctx.secret_store.acquire().await?, &password).await?;
|
||||
let fs = target_id
|
||||
@@ -159,7 +158,7 @@ pub async fn backup_all(
|
||||
if old_password.is_some() {
|
||||
backup_guard.change_password(&password)?;
|
||||
}
|
||||
let revision = assure_backing_up(&mut db, &package_ids).await?;
|
||||
assure_backing_up(&mut db, &package_ids).await?;
|
||||
tokio::task::spawn(async move {
|
||||
let backup_res = perform_backup(&ctx, &mut db, backup_guard, &package_ids).await;
|
||||
let backup_progress = crate::db::DatabaseModel::new()
|
||||
@@ -238,17 +237,14 @@ pub async fn backup_all(
|
||||
.await
|
||||
.expect("failed to change server status");
|
||||
});
|
||||
Ok(WithRevision {
|
||||
response: (),
|
||||
revision,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(db, packages))]
|
||||
async fn assure_backing_up(
|
||||
db: &mut PatchDbHandle,
|
||||
packages: impl IntoIterator<Item = &PackageId>,
|
||||
) -> Result<Option<Arc<Revision>>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let mut tx = db.begin().await?;
|
||||
let mut backing_up = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
@@ -279,7 +275,8 @@ async fn assure_backing_up(
|
||||
.collect(),
|
||||
);
|
||||
backing_up.save(&mut tx).await?;
|
||||
Ok(tx.commit(None).await?)
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, backup_guard))]
|
||||
|
||||
@@ -20,7 +20,6 @@ use super::target::BackupTargetId;
|
||||
use crate::backup::backup_bulk::OsBackup;
|
||||
use crate::context::{RpcContext, SetupContext};
|
||||
use crate::db::model::{PackageDataEntry, StaticFiles};
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::disk::mount::backup::{BackupMountGuard, PackageBackupMountGuard};
|
||||
use crate::disk::mount::filesystem::ReadOnly;
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
@@ -50,7 +49,7 @@ pub async fn restore_packages_rpc(
|
||||
#[arg(parse(parse_comma_separated))] ids: Vec<PackageId>,
|
||||
#[arg(rename = "target-id")] target_id: BackupTargetId,
|
||||
#[arg] password: String,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let fs = target_id
|
||||
.load(&mut ctx.secret_store.acquire().await?)
|
||||
@@ -114,10 +113,7 @@ pub async fn restore_packages_rpc(
|
||||
}
|
||||
});
|
||||
|
||||
Ok(WithRevision {
|
||||
response: (),
|
||||
revision,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn approximate_progress(
|
||||
@@ -418,7 +414,7 @@ async fn assure_restoring(
|
||||
guards.push((manifest, guard));
|
||||
}
|
||||
|
||||
Ok((tx.commit(None).await?, guards))
|
||||
Ok((tx.commit().await?, guards))
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, guard))]
|
||||
|
||||
@@ -7,11 +7,9 @@ use embassy::context::{DiagnosticContext, SetupContext};
|
||||
use embassy::disk::fsck::RepairStrategy;
|
||||
use embassy::disk::main::DEFAULT_PASSWORD;
|
||||
use embassy::disk::REPAIR_DISK_PATH;
|
||||
use embassy::hostname::get_product_key;
|
||||
use embassy::init::STANDBY_MODE_PATH;
|
||||
use embassy::middleware::cors::cors;
|
||||
use embassy::middleware::diagnostic::diagnostic;
|
||||
use embassy::middleware::encrypt::encrypt;
|
||||
#[cfg(feature = "avahi")]
|
||||
use embassy::net::mdns::MdnsController;
|
||||
use embassy::shutdown::Shutdown;
|
||||
@@ -50,12 +48,7 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
|
||||
.invoke(embassy::ErrorKind::Nginx)
|
||||
.await?;
|
||||
let ctx = SetupContext::init(cfg_path).await?;
|
||||
let keysource_ctx = ctx.clone();
|
||||
let keysource = move || {
|
||||
let ctx = keysource_ctx.clone();
|
||||
async move { ctx.product_key().await }
|
||||
};
|
||||
let encrypt = encrypt(keysource);
|
||||
let encrypt = embassy::middleware::encrypt::encrypt(ctx.clone());
|
||||
tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this
|
||||
CHIME.play().await?;
|
||||
rpc_server!({
|
||||
@@ -103,7 +96,7 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
|
||||
.await?;
|
||||
}
|
||||
tracing::info!("Loaded Disk");
|
||||
embassy::init::init(&cfg, &get_product_key().await?).await?;
|
||||
embassy::init::init(&cfg).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -7,6 +7,7 @@ use embassy::core::rpc_continuations::RequestGuid;
|
||||
use embassy::db::subscribe;
|
||||
use embassy::middleware::auth::auth;
|
||||
use embassy::middleware::cors::cors;
|
||||
use embassy::middleware::db::db as db_middleware;
|
||||
use embassy::middleware::diagnostic::diagnostic;
|
||||
#[cfg(feature = "avahi")]
|
||||
use embassy::net::mdns::MdnsController;
|
||||
@@ -40,7 +41,6 @@ fn err_to_500(e: Error) -> Response<Body> {
|
||||
#[instrument]
|
||||
async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
let (rpc_ctx, shutdown) = {
|
||||
embassy::hostname::sync_hostname().await?;
|
||||
let rpc_ctx = RpcContext::init(
|
||||
cfg_path,
|
||||
Arc::new(
|
||||
@@ -82,11 +82,13 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
});
|
||||
|
||||
let mut db = rpc_ctx.db.handle();
|
||||
embassy::hostname::sync_hostname(&mut db).await?;
|
||||
let receipts = embassy::context::rpc::RpcSetNginxReceipts::new(&mut db).await?;
|
||||
|
||||
rpc_ctx.set_nginx_conf(&mut db, receipts).await?;
|
||||
drop(db);
|
||||
let auth = auth(rpc_ctx.clone());
|
||||
let db_middleware = db_middleware(rpc_ctx.clone());
|
||||
let ctx = rpc_ctx.clone();
|
||||
let server = rpc_server!({
|
||||
command: embassy::main_api,
|
||||
@@ -95,6 +97,7 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
middleware: [
|
||||
cors,
|
||||
auth,
|
||||
db_middleware,
|
||||
]
|
||||
})
|
||||
.with_graceful_shutdown({
|
||||
@@ -112,29 +115,6 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
.await
|
||||
});
|
||||
|
||||
let rev_cache_ctx = rpc_ctx.clone();
|
||||
let revision_cache_task = tokio::spawn(async move {
|
||||
let mut sub = rev_cache_ctx.db.subscribe();
|
||||
let mut shutdown = rev_cache_ctx.shutdown.subscribe();
|
||||
loop {
|
||||
let rev = match tokio::select! {
|
||||
a = sub.recv() => a,
|
||||
_ = shutdown.recv() => break,
|
||||
} {
|
||||
Ok(a) => a,
|
||||
Err(_) => {
|
||||
rev_cache_ctx.revision_cache.write().await.truncate(0);
|
||||
continue;
|
||||
}
|
||||
}; // TODO: handle falling behind
|
||||
let mut cache = rev_cache_ctx.revision_cache.write().await;
|
||||
cache.push_back(rev);
|
||||
if cache.len() > rev_cache_ctx.revision_cache_size {
|
||||
cache.pop_front();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let ws_ctx = rpc_ctx.clone();
|
||||
let ws_server = {
|
||||
let builder = Server::bind(&ws_ctx.bind_ws);
|
||||
@@ -268,12 +248,6 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
ErrorKind::Unknown
|
||||
))
|
||||
.map_ok(|_| tracing::debug!("Metrics daemon Shutdown")),
|
||||
revision_cache_task
|
||||
.map_err(|e| Error::new(
|
||||
eyre!("{}", e).wrap_err("Revision Cache daemon panicked!"),
|
||||
ErrorKind::Unknown
|
||||
))
|
||||
.map_ok(|_| tracing::debug!("Revision Cache daemon Shutdown")),
|
||||
ws_server
|
||||
.map_err(|e| Error::new(e, ErrorKind::Network))
|
||||
.map_ok(|_| tracing::debug!("WebSocket Server Shutdown")),
|
||||
|
||||
@@ -15,7 +15,6 @@ use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::{CurrentDependencies, CurrentDependencyInfo, CurrentDependents};
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::dependencies::{
|
||||
add_dependent_to_current_dependents_lists, break_transitive, heal_all_dependents_transitive,
|
||||
BreakTransitiveReceipts, BreakageRes, Dependencies, DependencyConfig, DependencyError,
|
||||
@@ -237,7 +236,8 @@ pub async fn get(
|
||||
|
||||
#[command(
|
||||
subcommands(self(set_impl(async, context(RpcContext))), set_dry),
|
||||
display(display_none)
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
#[instrument]
|
||||
pub fn set(
|
||||
@@ -247,9 +247,8 @@ pub fn set(
|
||||
format: Option<IoFormat>,
|
||||
#[arg(long = "timeout")] timeout: Option<crate::util::serde::Duration>,
|
||||
#[arg(stdin, parse(parse_stdin_deserializable))] config: Option<Config>,
|
||||
#[arg(rename = "expire-id", long = "expire-id")] expire_id: Option<String>,
|
||||
) -> Result<(PackageId, Option<Config>, Option<Duration>, Option<String>), Error> {
|
||||
Ok((id, config, timeout.map(|d| *d), expire_id))
|
||||
) -> Result<(PackageId, Option<Config>, Option<Duration>), Error> {
|
||||
Ok((id, config, timeout.map(|d| *d)))
|
||||
}
|
||||
|
||||
/// So, the new locking finds all the possible locks and lifts them up into a bundle of locks.
|
||||
@@ -407,12 +406,7 @@ impl ConfigReceipts {
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn set_dry(
|
||||
#[context] ctx: RpcContext,
|
||||
#[parent_data] (id, config, timeout, _): (
|
||||
PackageId,
|
||||
Option<Config>,
|
||||
Option<Duration>,
|
||||
Option<String>,
|
||||
),
|
||||
#[parent_data] (id, config, timeout): (PackageId, Option<Config>, Option<Duration>),
|
||||
) -> Result<BreakageRes, Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
@@ -439,8 +433,8 @@ pub async fn set_dry(
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn set_impl(
|
||||
ctx: RpcContext,
|
||||
(id, config, timeout, expire_id): (PackageId, Option<Config>, Option<Duration>, Option<String>),
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
(id, config, timeout): (PackageId, Option<Config>, Option<Duration>),
|
||||
) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
let mut breakages = BTreeMap::new();
|
||||
@@ -457,10 +451,8 @@ pub async fn set_impl(
|
||||
&locks,
|
||||
)
|
||||
.await?;
|
||||
Ok(WithRevision {
|
||||
response: (),
|
||||
revision: tx.commit(expire_id).await?,
|
||||
})
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, receipts))]
|
||||
|
||||
@@ -22,7 +22,6 @@ use tracing::instrument;
|
||||
|
||||
use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation};
|
||||
use crate::db::model::{Database, InstalledPackageDataEntry, PackageDataEntry};
|
||||
use crate::hostname::{derive_hostname, derive_id, get_product_key};
|
||||
use crate::init::{init_postgres, pgloader};
|
||||
use crate::install::cleanup::{cleanup_failed, uninstall, CleanupFailedReceipts};
|
||||
use crate::manager::ManagerMap;
|
||||
@@ -71,23 +70,18 @@ impl RpcContextConfig {
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| Path::new("/embassy-data"))
|
||||
}
|
||||
pub async fn db(&self, secret_store: &PgPool, product_key: &str) -> Result<PatchDb, Error> {
|
||||
let sid = derive_id(product_key);
|
||||
let hostname = derive_hostname(&sid);
|
||||
pub async fn db(&self, secret_store: &PgPool) -> Result<PatchDb, Error> {
|
||||
let db_path = self.datadir().join("main").join("embassy.db");
|
||||
let db = PatchDb::open(&db_path)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?;
|
||||
if !db.exists(&<JsonPointer>::default()).await? {
|
||||
if !db.exists(&<JsonPointer>::default()).await {
|
||||
db.put(
|
||||
&<JsonPointer>::default(),
|
||||
&Database::init(
|
||||
sid,
|
||||
&hostname,
|
||||
&os_key(&mut secret_store.acquire().await?).await?,
|
||||
password_hash(&mut secret_store.acquire().await?).await?,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -216,7 +210,7 @@ impl RpcContext {
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
let secret_store = base.secret_store().await?;
|
||||
tracing::info!("Opened Pg DB");
|
||||
let db = base.db(&secret_store, &get_product_key().await?).await?;
|
||||
let db = base.db(&secret_store).await?;
|
||||
tracing::info!("Opened PatchDB");
|
||||
let docker = Docker::connect_with_unix_defaults()?;
|
||||
tracing::info!("Connected to Docker");
|
||||
@@ -231,6 +225,7 @@ impl RpcContext {
|
||||
.unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]),
|
||||
secret_store.clone(),
|
||||
None,
|
||||
&mut db.handle(),
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Initialized Net Controller");
|
||||
|
||||
@@ -2,29 +2,28 @@ use std::net::{IpAddr, SocketAddr};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use patch_db::PatchDb;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgConnectOptions;
|
||||
use sqlx::PgPool;
|
||||
use tokio::fs::File;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
use url::Host;
|
||||
|
||||
use crate::db::model::Database;
|
||||
use crate::hostname::{derive_hostname, derive_id, get_product_key};
|
||||
use crate::init::{init_postgres, pgloader};
|
||||
use crate::net::tor::os_key;
|
||||
use crate::setup::{password_hash, RecoveryStatus};
|
||||
use crate::util::io::from_yaml_async_reader;
|
||||
use crate::util::{AsyncFileExt, Invoke};
|
||||
use crate::util::AsyncFileExt;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -69,6 +68,9 @@ pub struct SetupContextSeed {
|
||||
pub bind_rpc: SocketAddr,
|
||||
pub shutdown: Sender<()>,
|
||||
pub datadir: PathBuf,
|
||||
/// Used to encrypt for hidding from snoopers for setups create password
|
||||
/// Set via path
|
||||
pub current_secret: RwLock<Option<String>>,
|
||||
pub selected_v2_drive: RwLock<Option<PathBuf>>,
|
||||
pub cached_product_key: RwLock<Option<Arc<String>>>,
|
||||
pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>,
|
||||
@@ -88,6 +90,7 @@ impl SetupContext {
|
||||
bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
|
||||
shutdown,
|
||||
datadir,
|
||||
current_secret: RwLock::new(None),
|
||||
selected_v2_drive: RwLock::new(None),
|
||||
cached_product_key: RwLock::new(None),
|
||||
recovery_status: RwLock::new(None),
|
||||
@@ -100,19 +103,13 @@ impl SetupContext {
|
||||
let db = PatchDb::open(&db_path)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?;
|
||||
if !db.exists(&<JsonPointer>::default()).await? {
|
||||
let pkey = self.product_key().await?;
|
||||
let sid = derive_id(&*pkey);
|
||||
let hostname = derive_hostname(&sid);
|
||||
if !db.exists(&<JsonPointer>::default()).await {
|
||||
db.put(
|
||||
&<JsonPointer>::default(),
|
||||
&Database::init(
|
||||
sid,
|
||||
&hostname,
|
||||
&os_key(&mut secret_store.acquire().await?).await?,
|
||||
password_hash(&mut secret_store.acquire().await?).await?,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -134,22 +131,17 @@ impl SetupContext {
|
||||
}
|
||||
Ok(secret_store)
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
pub async fn product_key(&self) -> Result<Arc<String>, Error> {
|
||||
Ok(
|
||||
if let Some(k) = {
|
||||
let guard = self.cached_product_key.read().await;
|
||||
let res = guard.clone();
|
||||
drop(guard);
|
||||
res
|
||||
} {
|
||||
k
|
||||
} else {
|
||||
let k = Arc::new(get_product_key().await?);
|
||||
*self.cached_product_key.write().await = Some(k.clone());
|
||||
k
|
||||
},
|
||||
)
|
||||
|
||||
/// So we assume that there will only be one client that will ask for a secret,
|
||||
/// And during that time do we upsert to a new key
|
||||
pub async fn update_secret(&self) -> Result<String, Error> {
|
||||
let new_secret: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(30)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
*self.current_secret.write().await = Some(new_secret.clone());
|
||||
Ok(new_secret)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use rpc_toolkit::command;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::dependencies::{
|
||||
break_all_dependents_transitive, heal_all_dependents_transitive, BreakageRes, DependencyError,
|
||||
DependencyReceipt, TaggedDependencyError,
|
||||
@@ -61,12 +60,9 @@ impl StartReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
#[command(display(display_none), metadata(sync_db = true))]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn start(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] id: PackageId,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
pub async fn start(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
let receipts = StartReceipts::new(&mut tx, &id).await?;
|
||||
@@ -77,7 +73,7 @@ pub async fn start(
|
||||
.await?;
|
||||
heal_all_dependents_transitive(&ctx, &mut tx, &id, &receipts.dependency_receipt).await?;
|
||||
|
||||
let revision = tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
drop(receipts);
|
||||
|
||||
ctx.managers
|
||||
@@ -87,10 +83,7 @@ pub async fn start(
|
||||
.synchronize()
|
||||
.await;
|
||||
|
||||
Ok(WithRevision {
|
||||
revision,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct StopReceipts {
|
||||
@@ -150,7 +143,11 @@ async fn stop_common<Db: DbHandle>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(subcommands(self(stop_impl(async)), stop_dry), display(display_none))]
|
||||
#[command(
|
||||
subcommands(self(stop_impl(async)), stop_dry),
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
pub fn stop(#[arg] id: PackageId) -> Result<PackageId, Error> {
|
||||
Ok(id)
|
||||
}
|
||||
@@ -173,23 +170,19 @@ pub async fn stop_dry(
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevision<()>, Error> {
|
||||
pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
stop_common(&mut tx, &id, &mut BTreeMap::new()).await?;
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: tx.commit(None).await?,
|
||||
response: (),
|
||||
})
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
pub async fn restart(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] id: PackageId,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
#[command(display(display_none), metadata(sync_db = true))]
|
||||
pub async fn restart(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
@@ -208,9 +201,7 @@ pub async fn restart(
|
||||
}
|
||||
*status = Some(MainStatus::Restarting);
|
||||
status.save(&mut tx).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: tx.commit(None).await?,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod model;
|
||||
pub mod package;
|
||||
pub mod util;
|
||||
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
@@ -14,7 +13,7 @@ use rpc_toolkit::hyper::{Body, Error as HyperError, Request, Response};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinError;
|
||||
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||
use tokio_tungstenite::tungstenite::protocol::CloseFrame;
|
||||
@@ -23,7 +22,6 @@ use tokio_tungstenite::WebSocketStream;
|
||||
use tracing::instrument;
|
||||
|
||||
pub use self::model::DatabaseModel;
|
||||
use self::util::WithRevision;
|
||||
use crate::context::RpcContext;
|
||||
use crate::middleware::auth::{HasValidSession, HashSessionToken};
|
||||
use crate::util::serde::{display_serializable, IoFormat};
|
||||
@@ -37,7 +35,7 @@ async fn ws_handler<
|
||||
session: Option<(HasValidSession, HashSessionToken)>,
|
||||
ws_fut: WSFut,
|
||||
) -> Result<(), Error> {
|
||||
let (dump, sub) = ctx.db.dump_and_sub().await;
|
||||
let (dump, sub) = ctx.db.dump_and_sub().await?;
|
||||
let mut stream = ws_fut
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?
|
||||
@@ -79,7 +77,7 @@ async fn subscribe_to_session_kill(
|
||||
async fn deal_with_messages(
|
||||
_has_valid_authentication: HasValidSession,
|
||||
mut kill: oneshot::Receiver<()>,
|
||||
mut sub: broadcast::Receiver<Arc<Revision>>,
|
||||
mut sub: patch_db::Subscriber,
|
||||
mut stream: WebSocketStream<Upgraded>,
|
||||
) -> Result<(), Error> {
|
||||
loop {
|
||||
@@ -95,8 +93,8 @@ async fn deal_with_messages(
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
return Ok(())
|
||||
}
|
||||
new_rev = sub.recv().fuse() => {
|
||||
let rev = new_rev.with_kind(crate::ErrorKind::Database)?;
|
||||
new_rev = sub.recv_async().fuse() => {
|
||||
let rev = new_rev.expect("UNREACHABLE: patch-db is dropped");
|
||||
stream
|
||||
.send(Message::Text(serde_json::to_string(&rev).with_kind(crate::ErrorKind::Serialization)?))
|
||||
.await
|
||||
@@ -184,24 +182,11 @@ pub async fn revisions(
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<RevisionsRes, RpcError> {
|
||||
let cache = ctx.revision_cache.read().await;
|
||||
if cache
|
||||
.front()
|
||||
.map(|rev| rev.id <= since + 1)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Ok(RevisionsRes::Revisions(
|
||||
cache
|
||||
.iter()
|
||||
.skip_while(|rev| rev.id < since + 1)
|
||||
.cloned()
|
||||
.collect(),
|
||||
))
|
||||
} else {
|
||||
drop(cache);
|
||||
Ok(RevisionsRes::Dump(ctx.db.dump().await))
|
||||
}
|
||||
) -> Result<RevisionsRes, Error> {
|
||||
Ok(match ctx.db.sync(since).await? {
|
||||
Ok(revs) => RevisionsRes::Revisions(revs),
|
||||
Err(dump) => RevisionsRes::Dump(dump),
|
||||
})
|
||||
}
|
||||
|
||||
#[command(display(display_serializable))]
|
||||
@@ -210,8 +195,8 @@ pub async fn dump(
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<Dump, RpcError> {
|
||||
Ok(ctx.db.dump().await)
|
||||
) -> Result<Dump, Error> {
|
||||
Ok(ctx.db.dump().await?)
|
||||
}
|
||||
|
||||
#[command(subcommands(ui))]
|
||||
@@ -228,13 +213,11 @@ pub async fn ui(
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let ptr = "/ui"
|
||||
.parse::<JsonPointer>()
|
||||
.with_kind(crate::ErrorKind::Database)?
|
||||
+ &pointer;
|
||||
Ok(WithRevision {
|
||||
response: (),
|
||||
revision: ctx.db.put(&ptr, &value, None).await?,
|
||||
})
|
||||
ctx.db.put(&ptr, &value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use serde_json::Value;
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
|
||||
use crate::config::spec::{PackagePointerSpec, SystemPointerSpec};
|
||||
use crate::hostname::{generate_hostname, generate_id};
|
||||
use crate::install::progress::InstallProgress;
|
||||
use crate::net::interface::InterfaceId;
|
||||
use crate::s9pk::manifest::{Manifest, ManifestModel, PackageId};
|
||||
@@ -32,21 +33,20 @@ pub struct Database {
|
||||
pub ui: Value,
|
||||
}
|
||||
impl Database {
|
||||
pub fn init(
|
||||
id: String,
|
||||
hostname: &str,
|
||||
tor_key: &TorSecretKeyV3,
|
||||
password_hash: String,
|
||||
) -> Self {
|
||||
pub fn init(tor_key: &TorSecretKeyV3, password_hash: String) -> Self {
|
||||
let id = generate_id();
|
||||
let my_hostname = generate_hostname();
|
||||
let lan_address = my_hostname.lan_address().parse().unwrap();
|
||||
// TODO
|
||||
Database {
|
||||
server_info: ServerInfo {
|
||||
id,
|
||||
version: Current::new().semver().into(),
|
||||
hostname: Some(my_hostname.0),
|
||||
last_backup: None,
|
||||
last_wifi_region: None,
|
||||
eos_version_compat: Current::new().compat().clone(),
|
||||
lan_address: format!("https://{}.local", hostname).parse().unwrap(),
|
||||
lan_address,
|
||||
tor_address: format!("http://{}", tor_key.public().get_onion_address())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
@@ -83,6 +83,7 @@ impl DatabaseModel {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ServerInfo {
|
||||
pub id: String,
|
||||
pub hostname: Option<String>,
|
||||
pub version: Version,
|
||||
pub last_backup: Option<DateTime<Utc>>,
|
||||
/// Used in the wifi to determine the region to set the system to
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use patch_db::Revision;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct WithRevision<T> {
|
||||
pub response: T,
|
||||
pub revision: Option<Arc<Revision>>,
|
||||
}
|
||||
@@ -1,34 +1,53 @@
|
||||
use digest::Digest;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use patch_db::DbHandle;
|
||||
use rand::{thread_rng, Rng};
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
use crate::{Error, ErrorKind};
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct Hostname(pub String);
|
||||
|
||||
pub const PRODUCT_KEY_PATH: &'static str = "/embassy-os/product_key.txt";
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_hostname() -> Result<String, Error> {
|
||||
Ok(derive_hostname(&get_id().await?))
|
||||
lazy_static::lazy_static! {
|
||||
static ref ADJECTIVES: Vec<String> = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect();
|
||||
static ref NOUNS: Vec<String> = include_str!("./assets/nouns.txt").lines().map(|x| x.to_string()).collect();
|
||||
}
|
||||
impl AsRef<str> for Hostname {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_hostname(id: &str) -> String {
|
||||
format!("embassy-{}", id)
|
||||
impl Hostname {
|
||||
pub fn lan_address(&self) -> String {
|
||||
format!("https://{}.local", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_hostname() -> Hostname {
|
||||
let mut rng = thread_rng();
|
||||
let adjective = &ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())];
|
||||
let noun = &NOUNS[rng.gen_range(0..NOUNS.len())];
|
||||
Hostname(format!("{adjective}-{noun}"))
|
||||
}
|
||||
|
||||
pub fn generate_id() -> String {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
id.to_string()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_current_hostname() -> Result<String, Error> {
|
||||
pub async fn get_current_hostname() -> Result<Hostname, Error> {
|
||||
let out = Command::new("hostname")
|
||||
.invoke(ErrorKind::ParseSysInfo)
|
||||
.await?;
|
||||
let out_string = String::from_utf8(out)?;
|
||||
Ok(out_string.trim().to_owned())
|
||||
Ok(Hostname(out_string.trim().to_owned()))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn set_hostname(hostname: &str) -> Result<(), Error> {
|
||||
pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> {
|
||||
let hostname: &String = &hostname.0;
|
||||
let _out = Command::new("hostnamectl")
|
||||
.arg("set-hostname")
|
||||
.arg(hostname)
|
||||
@@ -37,38 +56,36 @@ pub async fn set_hostname(hostname: &str) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_product_key() -> Result<String, Error> {
|
||||
let out = tokio::fs::read_to_string(PRODUCT_KEY_PATH)
|
||||
#[instrument(skip(handle))]
|
||||
pub async fn get_id<Db: DbHandle>(handle: &mut Db) -> Result<String, Error> {
|
||||
let id = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.id()
|
||||
.get(handle, false)
|
||||
.await?;
|
||||
Ok(id.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_hostname<Db: DbHandle>(handle: &mut Db) -> Result<Hostname, Error> {
|
||||
if let Ok(hostname) = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.hostname()
|
||||
.get(handle, false)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, PRODUCT_KEY_PATH))?;
|
||||
Ok(out.trim().to_owned())
|
||||
{
|
||||
if let Some(hostname) = hostname.to_owned() {
|
||||
return Ok(Hostname(hostname));
|
||||
}
|
||||
}
|
||||
let id = get_id(handle).await?;
|
||||
if id.len() != 8 {
|
||||
return Ok(generate_hostname());
|
||||
}
|
||||
return Ok(Hostname(format!("embassy-{}", id)));
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn set_product_key(key: &str) -> Result<(), Error> {
|
||||
let mut pkey_file = File::create(PRODUCT_KEY_PATH).await?;
|
||||
pkey_file.write_all(key.as_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn derive_id(key: &str) -> String {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(key.as_bytes());
|
||||
let res = hasher.finalize();
|
||||
hex::encode(&res[0..4])
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_id() -> Result<String, Error> {
|
||||
let key = get_product_key().await?;
|
||||
Ok(derive_id(&key))
|
||||
}
|
||||
|
||||
// cat /embassy-os/product_key.txt | shasum -a 256 | head -c 8 | awk '{print "embassy-"$1}' | xargs hostnamectl set-hostname && systemctl restart avahi-daemon
|
||||
#[instrument]
|
||||
pub async fn sync_hostname() -> Result<(), Error> {
|
||||
set_hostname(&format!("embassy-{}", get_id().await?)).await?;
|
||||
#[instrument(skip(handle))]
|
||||
pub async fn sync_hostname<Db: DbHandle>(handle: &mut Db) -> Result<(), Error> {
|
||||
set_hostname(&get_hostname(handle).await?).await?;
|
||||
Command::new("systemctl")
|
||||
.arg("restart")
|
||||
.arg("avahi-daemon")
|
||||
|
||||
@@ -8,7 +8,6 @@ use tokio::process::Command;
|
||||
|
||||
use crate::context::rpc::RpcContextConfig;
|
||||
use crate::db::model::ServerStatus;
|
||||
use crate::disk::mount::util::unmount;
|
||||
use crate::install::PKG_DOCKER_DIR;
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
@@ -152,7 +151,11 @@ pub async fn init_postgres(datadir: impl AsRef<Path>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error> {
|
||||
pub struct InitResult {
|
||||
pub db: patch_db::PatchDb,
|
||||
}
|
||||
|
||||
pub async fn init(cfg: &RpcContextConfig) -> Result<InitResult, Error> {
|
||||
let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok();
|
||||
let secret_store = cfg.secret_store().await?;
|
||||
let log_dir = cfg.datadir().join("main/logs");
|
||||
@@ -213,9 +216,14 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error
|
||||
|
||||
crate::ssh::sync_keys_from_db(&secret_store, "/home/start9/.ssh/authorized_keys").await?;
|
||||
tracing::info!("Synced SSH Keys");
|
||||
let db = cfg.db(&secret_store, product_key).await?;
|
||||
let db = cfg.db(&secret_store).await?;
|
||||
|
||||
let mut handle = db.handle();
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.lock(&mut handle, LockType::Write)
|
||||
.await?;
|
||||
|
||||
let receipts = InitReceipts::new(&mut handle).await?;
|
||||
|
||||
crate::net::wifi::synchronize_wpa_supplicant_conf(
|
||||
@@ -258,5 +266,5 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error
|
||||
|
||||
tracing::info!("System initialized.");
|
||||
|
||||
Ok(())
|
||||
Ok(InitResult { db })
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ where
|
||||
if tokio::fs::metadata(&volumes).await.is_ok() {
|
||||
tokio::fs::remove_dir_all(&volumes).await?;
|
||||
}
|
||||
tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
remove_tor_keys(secrets, &entry.manifest.id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ use crate::db::model::{
|
||||
CurrentDependencies, CurrentDependencyInfo, CurrentDependents, InstalledPackageDataEntry,
|
||||
PackageDataEntry, RecoveredPackageInfo, StaticDependencyInfo, StaticFiles,
|
||||
};
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::dependencies::{
|
||||
add_dependent_to_current_dependents_lists, break_all_dependents_transitive,
|
||||
reconfigure_dependents_with_live_pointers, BreakTransitiveReceipts, BreakageRes,
|
||||
@@ -115,7 +114,8 @@ impl std::fmt::Display for MinMax {
|
||||
|
||||
#[command(
|
||||
custom_cli(cli_install(async, context(CliContext))),
|
||||
display(display_none)
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn install(
|
||||
@@ -127,7 +127,7 @@ pub async fn install(
|
||||
String,
|
||||
>,
|
||||
#[arg(long = "version-priority", rename = "version-priority")] version_priority: Option<MinMax>,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let version_str = match &version_spec {
|
||||
None => "*",
|
||||
Some(v) => &*v,
|
||||
@@ -287,7 +287,7 @@ pub async fn install(
|
||||
}
|
||||
}
|
||||
pde.save(&mut tx).await?;
|
||||
let res = tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
drop(db_handle);
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -323,10 +323,7 @@ pub async fn install(
|
||||
}
|
||||
});
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: res,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(rpc_only, display(display_none))]
|
||||
@@ -427,7 +424,7 @@ pub async fn sideload(
|
||||
}
|
||||
}
|
||||
pde.save(&mut tx).await?;
|
||||
tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
if let Err(e) = download_install_s9pk(
|
||||
&new_ctx,
|
||||
@@ -559,7 +556,7 @@ async fn cli_install(
|
||||
ctx,
|
||||
"package.install",
|
||||
params,
|
||||
PhantomData::<WithRevision<()>>,
|
||||
PhantomData::<()>,
|
||||
)
|
||||
.await?
|
||||
.result?;
|
||||
@@ -570,7 +567,8 @@ async fn cli_install(
|
||||
|
||||
#[command(
|
||||
subcommands(self(uninstall_impl(async)), uninstall_dry),
|
||||
display(display_none)
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
pub async fn uninstall(#[arg] id: PackageId) -> Result<PackageId, Error> {
|
||||
Ok(id)
|
||||
@@ -601,7 +599,7 @@ pub async fn uninstall_dry(
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevision<()>, Error> {
|
||||
pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<(), Error> {
|
||||
let mut handle = ctx.db.handle();
|
||||
let mut tx = handle.begin().await?;
|
||||
|
||||
@@ -629,7 +627,7 @@ pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevisi
|
||||
removing: installed,
|
||||
});
|
||||
pde.save(&mut tx).await?;
|
||||
let res = tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
drop(handle);
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -666,17 +664,18 @@ pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevisi
|
||||
}
|
||||
});
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: res,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(rename = "delete-recovered", display(display_none))]
|
||||
#[command(
|
||||
rename = "delete-recovered",
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
pub async fn delete_recovered(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] id: PackageId,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let mut handle = ctx.db.handle();
|
||||
let mut tx = handle.begin().await?;
|
||||
let mut sql_tx = ctx.secret_store.begin().await?;
|
||||
@@ -699,13 +698,10 @@ pub async fn delete_recovered(
|
||||
}
|
||||
cleanup::remove_tor_keys(&mut sql_tx, &id).await?;
|
||||
|
||||
let res = tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
sql_tx.commit().await?;
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: res,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct DownloadInstallReceipts {
|
||||
@@ -858,7 +854,7 @@ pub async fn download_install_s9pk(
|
||||
tracing::error!("Failed to clean up {}@{}: {}", pkg_id, version, e);
|
||||
tracing::debug!("{:?}", e);
|
||||
} else {
|
||||
tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
}
|
||||
Err(e)
|
||||
} else {
|
||||
@@ -1147,7 +1143,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
if let Some(mut hdl) = rdr.scripts().await? {
|
||||
tokio::io::copy(
|
||||
&mut hdl,
|
||||
&mut File::create(dbg!(script_dir.join("embassy.js"))).await?,
|
||||
&mut File::create(script_dir.join("embassy.js")).await?,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1505,7 +1501,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
}
|
||||
|
||||
sql_tx.commit().await?;
|
||||
tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
tracing::info!("Install {}@{}: Complete", pkg_id, version);
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ pub struct LogResponse {
|
||||
end_cursor: Option<String>,
|
||||
}
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "kebab-case", tag = "type")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LogFollowResponse {
|
||||
start_cursor: Option<String>,
|
||||
guid: RequestGuid,
|
||||
|
||||
@@ -128,8 +128,7 @@ pub async fn check<Db: DbHandle>(
|
||||
|
||||
let status = receipts.status.get(&mut checkpoint).await?;
|
||||
|
||||
match status {
|
||||
MainStatus::Running { health, started } => {
|
||||
if let MainStatus::Running { health: _, started } = status {
|
||||
receipts
|
||||
.status
|
||||
.set(
|
||||
@@ -141,8 +140,6 @@ pub async fn check<Db: DbHandle>(
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
let current_dependents = receipts.current_dependents.get(&mut checkpoint).await?;
|
||||
|
||||
checkpoint.save().await?;
|
||||
|
||||
84
backend/src/middleware/db.rs
Normal file
84
backend/src/middleware/db.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
use http::HeaderValue;
|
||||
use rpc_toolkit::hyper::http::Error as HttpError;
|
||||
use rpc_toolkit::hyper::{Body, Request, Response};
|
||||
use rpc_toolkit::rpc_server_helpers::{
|
||||
noop4, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
|
||||
};
|
||||
use rpc_toolkit::yajrc::RpcMethod;
|
||||
use rpc_toolkit::Metadata;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub fn db<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
||||
Box::new(
|
||||
move |_: &mut Request<Body>,
|
||||
metadata: M|
|
||||
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
|
||||
let ctx = ctx.clone();
|
||||
async move {
|
||||
let m2: DynMiddlewareStage2 = Box::new(move |req, rpc_req| {
|
||||
async move {
|
||||
let seq = req.headers.remove("x-patch-sequence");
|
||||
let sync_db = metadata
|
||||
.get(rpc_req.method.as_str(), "sync_db")
|
||||
.unwrap_or(false);
|
||||
|
||||
let m3: DynMiddlewareStage3 = Box::new(move |res, _| {
|
||||
async move {
|
||||
if sync_db && seq.is_some() {
|
||||
match async {
|
||||
let seq = seq
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Missing X-Patch-Sequence"),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
)
|
||||
})?
|
||||
.to_str()
|
||||
.with_kind(crate::ErrorKind::InvalidRequest)?
|
||||
.parse()?;
|
||||
let res = ctx.db.sync(seq).await?;
|
||||
let json = match res {
|
||||
Ok(revs) => serde_json::to_vec(&revs),
|
||||
Err(dump) => serde_json::to_vec(&[dump]),
|
||||
}
|
||||
.with_kind(crate::ErrorKind::Serialization)?;
|
||||
Ok::<_, Error>(
|
||||
url::form_urlencoded::byte_serialize(&json)
|
||||
.collect::<String>(),
|
||||
)
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(a) => res
|
||||
.headers
|
||||
.append("X-Patch-Updates", HeaderValue::from_str(&a)?),
|
||||
Err(e) => res.headers.append(
|
||||
"X-Patch-Error",
|
||||
HeaderValue::from_str(
|
||||
&url::form_urlencoded::byte_serialize(
|
||||
e.to_string().as_bytes(),
|
||||
)
|
||||
.collect::<String>(),
|
||||
)?,
|
||||
),
|
||||
};
|
||||
}
|
||||
Ok(Ok(noop4()))
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
Ok(Ok(m3))
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
Ok(Ok(m2))
|
||||
}
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher};
|
||||
@@ -17,6 +16,7 @@ use rpc_toolkit::yajrc::RpcMethod;
|
||||
use rpc_toolkit::Metadata;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::context::SetupContext;
|
||||
use crate::util::Apply;
|
||||
use crate::Error;
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn encrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec
|
||||
let prefix: [u8; 32] = rand::random();
|
||||
let aeskey = pbkdf2(password.as_ref(), &prefix[16..]);
|
||||
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
|
||||
let mut aes = Aes256Ctr::new(&aeskey, &ctr);
|
||||
let mut aes = Aes256Ctr::new(&aeskey, ctr);
|
||||
let mut res = Vec::with_capacity(32 + input.as_ref().len());
|
||||
res.extend_from_slice(&prefix[..]);
|
||||
res.extend_from_slice(input.as_ref());
|
||||
@@ -50,7 +50,7 @@ pub fn decrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec
|
||||
let (prefix, rest) = input.as_ref().split_at(32);
|
||||
let aeskey = pbkdf2(password.as_ref(), &prefix[16..]);
|
||||
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
|
||||
let mut aes = Aes256Ctr::new(&aeskey, &ctr);
|
||||
let mut aes = Aes256Ctr::new(&aeskey, ctr);
|
||||
let mut res = rest.to_vec();
|
||||
aes.apply_keystream(&mut res);
|
||||
res
|
||||
@@ -92,20 +92,20 @@ impl Stream for DecryptStream {
|
||||
aes.apply_keystream(&mut res);
|
||||
res.into()
|
||||
} else {
|
||||
if this.ctr.len() < 16 && buf.len() > 0 {
|
||||
if this.ctr.len() < 16 && !buf.is_empty() {
|
||||
let to_read = std::cmp::min(16 - this.ctr.len(), buf.len());
|
||||
this.ctr.extend_from_slice(&buf[0..to_read]);
|
||||
buf = &buf[to_read..];
|
||||
}
|
||||
if this.salt.len() < 16 && buf.len() > 0 {
|
||||
if this.salt.len() < 16 && !buf.is_empty() {
|
||||
let to_read = std::cmp::min(16 - this.salt.len(), buf.len());
|
||||
this.salt.extend_from_slice(&buf[0..to_read]);
|
||||
buf = &buf[to_read..];
|
||||
}
|
||||
if this.ctr.len() == 16 && this.salt.len() == 16 {
|
||||
let aeskey = pbkdf2(this.key.as_bytes(), &this.salt);
|
||||
let ctr = Nonce::<Aes256Ctr>::from_slice(&this.ctr);
|
||||
let mut aes = Aes256Ctr::new(&aeskey, &ctr);
|
||||
let ctr = Nonce::<Aes256Ctr>::from_slice(this.ctr);
|
||||
let mut aes = Aes256Ctr::new(&aeskey, ctr);
|
||||
let mut res = buf.to_vec();
|
||||
aes.apply_keystream(&mut res);
|
||||
*this.aes = Some(aes);
|
||||
@@ -132,7 +132,7 @@ impl EncryptStream {
|
||||
let prefix: [u8; 32] = rand::random();
|
||||
let aeskey = pbkdf2(key.as_bytes(), &prefix[16..]);
|
||||
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
|
||||
let aes = Aes256Ctr::new(&aeskey, &ctr);
|
||||
let aes = Aes256Ctr::new(&aeskey, ctr);
|
||||
EncryptStream {
|
||||
body,
|
||||
aes,
|
||||
@@ -169,42 +169,42 @@ fn encrypted(headers: &HeaderMap) -> bool {
|
||||
.and_then(|h| {
|
||||
h.to_str()
|
||||
.ok()?
|
||||
.split(",")
|
||||
.split(',')
|
||||
.any(|s| s == "aesctr256")
|
||||
.apply(Some)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn encrypt<
|
||||
F: Fn() -> Fut + Send + Sync + Clone + 'static,
|
||||
Fut: Future<Output = Result<Arc<String>, Error>> + Send + Sync + 'static,
|
||||
M: Metadata,
|
||||
>(
|
||||
keysource: F,
|
||||
) -> DynMiddleware<M> {
|
||||
pub fn encrypt<M: Metadata>(ctx: SetupContext) -> DynMiddleware<M> {
|
||||
Box::new(
|
||||
move |req: &mut Request<Body>,
|
||||
metadata: M|
|
||||
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
|
||||
let keysource = keysource.clone();
|
||||
let keysource = ctx.clone();
|
||||
async move {
|
||||
let encrypted = encrypted(req.headers());
|
||||
let current_secret: Option<String> = keysource.current_secret.read().await.clone();
|
||||
let key = if encrypted {
|
||||
let key = match keysource().await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let key = match current_secret {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
let (res_parts, _) = Response::new(()).into_parts();
|
||||
return Ok(Err(to_response(
|
||||
req.headers(),
|
||||
res_parts,
|
||||
Err(e.into()),
|
||||
Err(Error::new(
|
||||
eyre!("No Secret has been set"),
|
||||
crate::ErrorKind::RateLimited,
|
||||
)
|
||||
.into()),
|
||||
|_| StatusCode::OK,
|
||||
)?));
|
||||
}
|
||||
};
|
||||
let body = std::mem::take(req.body_mut());
|
||||
*req.body_mut() = Body::wrap_stream(DecryptStream::new(key.clone(), body));
|
||||
*req.body_mut() =
|
||||
Body::wrap_stream(DecryptStream::new(Arc::new(key.clone()), body));
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
@@ -213,7 +213,7 @@ pub fn encrypt<
|
||||
async move {
|
||||
if !encrypted
|
||||
&& metadata
|
||||
.get(&rpc_req.method.as_str(), "authenticated")
|
||||
.get(rpc_req.method.as_str(), "authenticated")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let (res_parts, _) = Response::new(()).into_parts();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod cors;
|
||||
pub mod db;
|
||||
pub mod diagnostic;
|
||||
pub mod encrypt;
|
||||
|
||||
@@ -3,14 +3,14 @@ use std::net::Ipv4Addr;
|
||||
|
||||
use avahi_sys::{
|
||||
self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit,
|
||||
avahi_entry_group_free, avahi_entry_group_reset, avahi_free, avahi_strerror, AvahiClient,
|
||||
AvahiEntryGroup,
|
||||
avahi_entry_group_free, avahi_free, avahi_strerror, AvahiClient, AvahiEntryGroup,
|
||||
};
|
||||
use color_eyre::eyre::eyre;
|
||||
use libc::c_void;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::Mutex;
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::interface::InterfaceId;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
@@ -59,17 +59,64 @@ impl MdnsController {
|
||||
}
|
||||
|
||||
pub struct MdnsControllerInner {
|
||||
hostname: Vec<u8>,
|
||||
hostname_raw: *const libc::c_char,
|
||||
entry_group: *mut AvahiEntryGroup,
|
||||
entry_group: Option<MdnsEntryGroup>,
|
||||
services: BTreeMap<(PackageId, InterfaceId), TorSecretKeyV3>,
|
||||
_client_error: std::pin::Pin<Box<i32>>,
|
||||
}
|
||||
unsafe impl Send for MdnsControllerInner {}
|
||||
unsafe impl Sync for MdnsControllerInner {}
|
||||
|
||||
impl MdnsControllerInner {
|
||||
fn load_services(&mut self) {
|
||||
fn init() -> Self {
|
||||
MdnsControllerInner {
|
||||
entry_group: Some(MdnsEntryGroup::init(&BTreeMap::new())),
|
||||
services: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
fn sync(&mut self) {
|
||||
drop(self.entry_group.take());
|
||||
self.entry_group = Some(MdnsEntryGroup::init(&self.services));
|
||||
}
|
||||
fn add<'a, I: IntoIterator<Item = (InterfaceId, TorSecretKeyV3)>>(
|
||||
&mut self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: I,
|
||||
) {
|
||||
self.services.extend(
|
||||
interfaces
|
||||
.into_iter()
|
||||
.map(|(interface_id, key)| ((pkg_id.clone(), interface_id), key)),
|
||||
);
|
||||
self.sync();
|
||||
}
|
||||
fn remove<I: IntoIterator<Item = InterfaceId>>(&mut self, pkg_id: &PackageId, interfaces: I) {
|
||||
for interface_id in interfaces {
|
||||
self.services.remove(&(pkg_id.clone(), interface_id));
|
||||
}
|
||||
self.sync();
|
||||
}
|
||||
fn free(&self) {}
|
||||
}
|
||||
|
||||
fn log_str_error(action: &str, e: i32) {
|
||||
unsafe {
|
||||
let e_str = avahi_strerror(e);
|
||||
tracing::error!(
|
||||
"Could not {}: {:?}",
|
||||
action,
|
||||
std::ffi::CStr::from_ptr(e_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct MdnsEntryGroup {
|
||||
hostname: Vec<u8>,
|
||||
hostname_raw: *const libc::c_char,
|
||||
entry_group: *mut AvahiEntryGroup,
|
||||
_client_error: std::pin::Pin<Box<i32>>,
|
||||
}
|
||||
impl MdnsEntryGroup {
|
||||
#[instrument(skip(self))]
|
||||
fn load_services(&mut self, services: &BTreeMap<(PackageId, InterfaceId), TorSecretKeyV3>) {
|
||||
unsafe {
|
||||
tracing::debug!("Loading services for mDNS");
|
||||
let mut res;
|
||||
@@ -101,7 +148,7 @@ impl MdnsControllerInner {
|
||||
"Published {:?}",
|
||||
std::ffi::CStr::from_ptr(self.hostname_raw)
|
||||
);
|
||||
for key in self.services.values() {
|
||||
for key in services.values() {
|
||||
let lan_address = key
|
||||
.public()
|
||||
.get_onion_address()
|
||||
@@ -131,9 +178,10 @@ impl MdnsControllerInner {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn init() -> Self {
|
||||
fn init(services: &BTreeMap<(PackageId, InterfaceId), TorSecretKeyV3>) -> Self {
|
||||
unsafe {
|
||||
tracing::debug!("Initializing mDNS controller");
|
||||
|
||||
let simple_poll = avahi_sys::avahi_simple_poll_new();
|
||||
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
|
||||
let mut box_err = Box::pin(0 as i32);
|
||||
@@ -168,15 +216,13 @@ impl MdnsControllerInner {
|
||||
// assume fixed length prefix on hostname due to local address
|
||||
hostname_buf[0] = (buflen - 8) as u8; // set the prefix length to len - 8 (leading byte, .local, nul) for the main address
|
||||
hostname_buf[buflen - 7] = 5; // set the prefix length to 5 for "local"
|
||||
|
||||
let mut res = MdnsControllerInner {
|
||||
let mut res = MdnsEntryGroup {
|
||||
hostname: hostname_buf,
|
||||
hostname_raw,
|
||||
entry_group: group,
|
||||
services: BTreeMap::new(),
|
||||
_client_error: box_err,
|
||||
};
|
||||
res.load_services();
|
||||
res.load_services(services);
|
||||
let commit_err = avahi_entry_group_commit(res.entry_group);
|
||||
if commit_err < avahi_sys::AVAHI_OK {
|
||||
log_str_error("reset Avahi entry group", commit_err);
|
||||
@@ -185,62 +231,17 @@ impl MdnsControllerInner {
|
||||
res
|
||||
}
|
||||
}
|
||||
fn sync(&mut self) {
|
||||
unsafe {
|
||||
let mut res;
|
||||
res = avahi_entry_group_reset(self.entry_group);
|
||||
if res < avahi_sys::AVAHI_OK {
|
||||
log_str_error("reset Avahi entry group", res);
|
||||
panic!("Failed to load Avahi services: reset");
|
||||
}
|
||||
self.load_services();
|
||||
res = avahi_entry_group_commit(self.entry_group);
|
||||
if res < avahi_sys::AVAHI_OK {
|
||||
log_str_error("commit Avahi entry group", res);
|
||||
panic!("Failed to load Avahi services: commit");
|
||||
}
|
||||
}
|
||||
}
|
||||
fn add<'a, I: IntoIterator<Item = (InterfaceId, TorSecretKeyV3)>>(
|
||||
&mut self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: I,
|
||||
) {
|
||||
self.services.extend(
|
||||
interfaces
|
||||
.into_iter()
|
||||
.map(|(interface_id, key)| ((pkg_id.clone(), interface_id), key)),
|
||||
);
|
||||
self.sync();
|
||||
}
|
||||
fn remove<I: IntoIterator<Item = InterfaceId>>(&mut self, pkg_id: &PackageId, interfaces: I) {
|
||||
for interface_id in interfaces {
|
||||
self.services.remove(&(pkg_id.clone(), interface_id));
|
||||
}
|
||||
self.sync();
|
||||
}
|
||||
}
|
||||
impl Drop for MdnsControllerInner {
|
||||
impl Drop for MdnsEntryGroup {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
avahi_free(self.hostname_raw as *mut c_void);
|
||||
avahi_entry_group_free(self.entry_group);
|
||||
// avahi_client_free(self.avahi_client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn log_str_error(action: &str, e: i32) {
|
||||
unsafe {
|
||||
let e_str = avahi_strerror(e);
|
||||
tracing::error!(
|
||||
"Could not {}: {:?}",
|
||||
action,
|
||||
std::ffi::CStr::from_ptr(e_str)
|
||||
);
|
||||
avahi_free(e_str as *mut c_void);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn entry_group_callback(
|
||||
_group: *mut avahi_sys::AvahiEntryGroup,
|
||||
state: avahi_sys::AvahiEntryGroupState,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::X509;
|
||||
use patch_db::DbHandle;
|
||||
use rpc_toolkit::command;
|
||||
use sqlx::PgPool;
|
||||
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
|
||||
@@ -14,6 +15,7 @@ use self::mdns::MdnsController;
|
||||
use self::nginx::NginxController;
|
||||
use self::ssl::SslManager;
|
||||
use self::tor::TorController;
|
||||
use crate::hostname::get_hostname;
|
||||
use crate::net::dns::DnsController;
|
||||
use crate::net::interface::TorConfig;
|
||||
use crate::net::nginx::InterfaceMetadata;
|
||||
@@ -50,24 +52,26 @@ pub struct NetController {
|
||||
pub dns: DnsController,
|
||||
}
|
||||
impl NetController {
|
||||
#[instrument(skip(db))]
|
||||
pub async fn init(
|
||||
#[instrument(skip(db, handle))]
|
||||
pub async fn init<Db: DbHandle>(
|
||||
embassyd_addr: SocketAddr,
|
||||
embassyd_tor_key: TorSecretKeyV3,
|
||||
tor_control: SocketAddr,
|
||||
dns_bind: &[SocketAddr],
|
||||
db: PgPool,
|
||||
import_root_ca: Option<(PKey<Private>, X509)>,
|
||||
handle: &mut Db,
|
||||
) -> Result<Self, Error> {
|
||||
let ssl = match import_root_ca {
|
||||
None => SslManager::init(db).await,
|
||||
None => SslManager::init(db, handle).await,
|
||||
Some(a) => SslManager::import_root_ca(db, a.0, a.1).await,
|
||||
}?;
|
||||
let hostname = get_hostname(handle).await?;
|
||||
Ok(Self {
|
||||
tor: TorController::init(embassyd_addr, embassyd_tor_key, tor_control).await?,
|
||||
#[cfg(feature = "avahi")]
|
||||
mdns: MdnsController::init(),
|
||||
nginx: NginxController::init(PathBuf::from("/etc/nginx"), &ssl).await?,
|
||||
nginx: NginxController::init(PathBuf::from("/etc/nginx"), &ssl, &hostname).await?,
|
||||
ssl,
|
||||
dns: DnsController::init(dns_bind).await?,
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ use tracing::instrument;
|
||||
|
||||
use super::interface::{InterfaceId, LanPortConfig};
|
||||
use super::ssl::SslManager;
|
||||
use crate::hostname::get_hostname;
|
||||
use crate::hostname::Hostname;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::Port;
|
||||
use crate::util::Invoke;
|
||||
@@ -20,9 +20,15 @@ pub struct NginxController {
|
||||
inner: Mutex<NginxControllerInner>,
|
||||
}
|
||||
impl NginxController {
|
||||
pub async fn init(nginx_root: PathBuf, ssl_manager: &SslManager) -> Result<Self, Error> {
|
||||
pub async fn init(
|
||||
nginx_root: PathBuf,
|
||||
ssl_manager: &SslManager,
|
||||
host_name: &Hostname,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(NginxController {
|
||||
inner: Mutex::new(NginxControllerInner::init(&nginx_root, ssl_manager).await?),
|
||||
inner: Mutex::new(
|
||||
NginxControllerInner::init(&nginx_root, ssl_manager, host_name).await?,
|
||||
),
|
||||
nginx_root,
|
||||
})
|
||||
}
|
||||
@@ -53,13 +59,17 @@ pub struct NginxControllerInner {
|
||||
}
|
||||
impl NginxControllerInner {
|
||||
#[instrument]
|
||||
async fn init(nginx_root: &Path, ssl_manager: &SslManager) -> Result<Self, Error> {
|
||||
async fn init(
|
||||
nginx_root: &Path,
|
||||
ssl_manager: &SslManager,
|
||||
host_name: &Hostname,
|
||||
) -> Result<Self, Error> {
|
||||
let inner = NginxControllerInner {
|
||||
interfaces: BTreeMap::new(),
|
||||
};
|
||||
// write main ssl key/cert to fs location
|
||||
let (key, cert) = ssl_manager
|
||||
.certificate_for(&get_hostname().await?, &"embassy".parse().unwrap())
|
||||
.certificate_for(&host_name.lan_address(), &"embassy".parse().unwrap())
|
||||
.await?;
|
||||
let ssl_path_key = nginx_root.join(format!("ssl/embassy_main.key.pem"));
|
||||
let ssl_path_cert = nginx_root.join(format!("ssl/embassy_main.cert.pem"));
|
||||
|
||||
@@ -11,6 +11,7 @@ use openssl::nid::Nid;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509};
|
||||
use openssl::*;
|
||||
use patch_db::DbHandle;
|
||||
use sqlx::PgPool;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -161,13 +162,14 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
impl SslManager {
|
||||
#[instrument(skip(db))]
|
||||
pub async fn init(db: PgPool) -> Result<Self, Error> {
|
||||
#[instrument(skip(db, handle))]
|
||||
pub async fn init<Db: DbHandle>(db: PgPool, handle: &mut Db) -> Result<Self, Error> {
|
||||
let store = SslStore::new(db)?;
|
||||
let id = crate::hostname::get_id(handle).await?;
|
||||
let (root_key, root_cert) = match store.load_root_certificate().await? {
|
||||
None => {
|
||||
let root_key = generate_key()?;
|
||||
let server_id = crate::hostname::get_id().await?;
|
||||
let server_id = id;
|
||||
let root_cert = make_root_cert(&root_key, &server_id)?;
|
||||
store.save_root_certificate(&root_key, &root_cert).await?;
|
||||
Ok::<_, Error>((root_key, root_cert))
|
||||
@@ -511,56 +513,56 @@ fn make_leaf_cert(
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ca_details_persist() -> Result<(), Error> {
|
||||
let pool = sqlx::Pool::<sqlx::Postgres>::connect("postgres::memory:").await?;
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Database)?;
|
||||
let mgr = SslManager::init(pool.clone()).await?;
|
||||
let root_cert0 = mgr.root_cert;
|
||||
let int_key0 = mgr.int_key;
|
||||
let int_cert0 = mgr.int_cert;
|
||||
let mgr = SslManager::init(pool).await?;
|
||||
let root_cert1 = mgr.root_cert;
|
||||
let int_key1 = mgr.int_key;
|
||||
let int_cert1 = mgr.int_cert;
|
||||
|
||||
assert_eq!(root_cert0.to_pem()?, root_cert1.to_pem()?);
|
||||
assert_eq!(
|
||||
int_key0.private_key_to_pem_pkcs8()?,
|
||||
int_key1.private_key_to_pem_pkcs8()?
|
||||
);
|
||||
assert_eq!(int_cert0.to_pem()?, int_cert1.to_pem()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn certificate_details_persist() -> Result<(), Error> {
|
||||
let pool = sqlx::Pool::<sqlx::Postgres>::connect("postgres::memory:").await?;
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Database)?;
|
||||
let mgr = SslManager::init(pool.clone()).await?;
|
||||
let package_id = "bitcoind".parse().unwrap();
|
||||
let (key0, cert_chain0) = mgr.certificate_for("start9", &package_id).await?;
|
||||
let (key1, cert_chain1) = mgr.certificate_for("start9", &package_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
key0.private_key_to_pem_pkcs8()?,
|
||||
key1.private_key_to_pem_pkcs8()?
|
||||
);
|
||||
assert_eq!(
|
||||
cert_chain0
|
||||
.iter()
|
||||
.map(|cert| cert.to_pem().unwrap())
|
||||
.collect::<Vec<Vec<u8>>>(),
|
||||
cert_chain1
|
||||
.iter()
|
||||
.map(|cert| cert.to_pem().unwrap())
|
||||
.collect::<Vec<Vec<u8>>>()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
// #[tokio::test]
|
||||
// async fn ca_details_persist() -> Result<(), Error> {
|
||||
// let pool = sqlx::Pool::<sqlx::Postgres>::connect("postgres::memory:").await?;
|
||||
// sqlx::migrate!()
|
||||
// .run(&pool)
|
||||
// .await
|
||||
// .with_kind(crate::ErrorKind::Database)?;
|
||||
// let mgr = SslManager::init(pool.clone()).await?;
|
||||
// let root_cert0 = mgr.root_cert;
|
||||
// let int_key0 = mgr.int_key;
|
||||
// let int_cert0 = mgr.int_cert;
|
||||
// let mgr = SslManager::init(pool).await?;
|
||||
// let root_cert1 = mgr.root_cert;
|
||||
// let int_key1 = mgr.int_key;
|
||||
// let int_cert1 = mgr.int_cert;
|
||||
//
|
||||
// assert_eq!(root_cert0.to_pem()?, root_cert1.to_pem()?);
|
||||
// assert_eq!(
|
||||
// int_key0.private_key_to_pem_pkcs8()?,
|
||||
// int_key1.private_key_to_pem_pkcs8()?
|
||||
// );
|
||||
// assert_eq!(int_cert0.to_pem()?, int_cert1.to_pem()?);
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[tokio::test]
|
||||
// async fn certificate_details_persist() -> Result<(), Error> {
|
||||
// let pool = sqlx::Pool::<sqlx::Postgres>::connect("postgres::memory:").await?;
|
||||
// sqlx::migrate!()
|
||||
// .run(&pool)
|
||||
// .await
|
||||
// .with_kind(crate::ErrorKind::Database)?;
|
||||
// let mgr = SslManager::init(pool.clone()).await?;
|
||||
// let package_id = "bitcoind".parse().unwrap();
|
||||
// let (key0, cert_chain0) = mgr.certificate_for("start9", &package_id).await?;
|
||||
// let (key1, cert_chain1) = mgr.certificate_for("start9", &package_id).await?;
|
||||
//
|
||||
// assert_eq!(
|
||||
// key0.private_key_to_pem_pkcs8()?,
|
||||
// key1.private_key_to_pem_pkcs8()?
|
||||
// );
|
||||
// assert_eq!(
|
||||
// cert_chain0
|
||||
// .iter()
|
||||
// .map(|cert| cert.to_pem().unwrap())
|
||||
// .collect::<Vec<Vec<u8>>>(),
|
||||
// cert_chain1
|
||||
// .iter()
|
||||
// .map(|cert| cert.to_pem().unwrap())
|
||||
// .collect::<Vec<Vec<u8>>>()
|
||||
// );
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
@@ -16,6 +16,8 @@ server {{
|
||||
|
||||
server_name .{lan_hostname};
|
||||
|
||||
proxy_buffers 4 512k;
|
||||
proxy_buffer_size 512k;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_socket_keepalive on;
|
||||
@@ -71,6 +73,8 @@ server {{
|
||||
|
||||
server_name .{tor_hostname};
|
||||
|
||||
proxy_buffers 4 512k;
|
||||
proxy_buffer_size 512k;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_socket_keepalive on;
|
||||
|
||||
@@ -12,7 +12,6 @@ use tracing::instrument;
|
||||
|
||||
use crate::backup::BackupReport;
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::display_none;
|
||||
use crate::util::serde::display_serializable;
|
||||
@@ -29,7 +28,7 @@ pub async fn list(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] before: Option<i32>,
|
||||
#[arg] limit: Option<u32>,
|
||||
) -> Result<WithRevision<Vec<Notification>>, Error> {
|
||||
) -> Result<Vec<Notification>, Error> {
|
||||
let limit = limit.unwrap_or(40);
|
||||
let mut handle = ctx.db.handle();
|
||||
match before {
|
||||
@@ -72,11 +71,8 @@ pub async fn list(
|
||||
})
|
||||
.collect::<Result<Vec<Notification>, Error>>()?;
|
||||
// set notification count to zero
|
||||
let r = model.put(&mut handle, &0).await?;
|
||||
Ok(WithRevision {
|
||||
response: notifs,
|
||||
revision: r,
|
||||
})
|
||||
model.put(&mut handle, &0).await?;
|
||||
Ok(notifs)
|
||||
}
|
||||
Some(before) => {
|
||||
let records = sqlx::query!(
|
||||
@@ -113,10 +109,7 @@ pub async fn list(
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<Notification>, Error>>()?;
|
||||
Ok(WithRevision {
|
||||
response: res,
|
||||
revision: None,
|
||||
})
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use digest::generic_array::GenericArray;
|
||||
use digest::OutputSizeUser;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{FutureExt, TryFutureExt, TryStreamExt};
|
||||
use josekit::jwk::Jwk;
|
||||
use nix::unistd::{Gid, Uid};
|
||||
use openssl::x509::X509;
|
||||
use patch_db::{DbHandle, LockType};
|
||||
@@ -37,7 +38,7 @@ use crate::disk::mount::filesystem::ReadOnly;
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
use crate::disk::util::{pvscan, recovery_info, DiskListResponse, EmbassyOsRecoveryInfo};
|
||||
use crate::disk::REPAIR_DISK_PATH;
|
||||
use crate::hostname::PRODUCT_KEY_PATH;
|
||||
use crate::hostname::{get_hostname, Hostname};
|
||||
use crate::id::Id;
|
||||
use crate::init::init;
|
||||
use crate::install::PKG_PUBLIC_DIR;
|
||||
@@ -62,7 +63,7 @@ where
|
||||
Ok(password)
|
||||
}
|
||||
|
||||
#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete))]
|
||||
#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete, get_secret))]
|
||||
pub fn setup() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -70,14 +71,12 @@ pub fn setup() -> Result<(), Error> {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct StatusRes {
|
||||
product_key: bool,
|
||||
migrating: bool,
|
||||
}
|
||||
|
||||
#[command(rpc_only, metadata(authenticated = false))]
|
||||
pub async fn status(#[context] ctx: SetupContext) -> Result<StatusRes, Error> {
|
||||
Ok(StatusRes {
|
||||
product_key: tokio::fs::metadata(PRODUCT_KEY_PATH).await.is_ok(),
|
||||
migrating: ctx.recovery_status.read().await.is_some(),
|
||||
})
|
||||
}
|
||||
@@ -123,31 +122,7 @@ pub async fn attach(
|
||||
ErrorKind::DiskManagement,
|
||||
));
|
||||
}
|
||||
let product_key = ctx.product_key().await?;
|
||||
let product_key_path = Path::new("/embassy-data/main/product_key.txt");
|
||||
if tokio::fs::metadata(product_key_path).await.is_ok() {
|
||||
let pkey = Arc::new(
|
||||
tokio::fs::read_to_string(product_key_path)
|
||||
.await?
|
||||
.trim()
|
||||
.to_owned(),
|
||||
);
|
||||
if pkey != product_key {
|
||||
crate::disk::main::export(&*guid, &ctx.datadir).await?;
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"The EmbassyOS product key does not match the supplied drive: {}",
|
||||
pkey
|
||||
),
|
||||
ErrorKind::ProductKeyMismatch,
|
||||
));
|
||||
}
|
||||
}
|
||||
init(
|
||||
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
|
||||
&*product_key,
|
||||
)
|
||||
.await?;
|
||||
init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?).await?;
|
||||
let secrets = ctx.secret_store().await?;
|
||||
let db = ctx.db(&secrets).await?;
|
||||
let mut secrets_handle = secrets.acquire().await?;
|
||||
@@ -168,16 +143,17 @@ pub async fn attach(
|
||||
|
||||
let tor_key = crate::net::tor::os_key(&mut secrets_tx).await?;
|
||||
|
||||
db_tx.commit(None).await?;
|
||||
db_tx.commit().await?;
|
||||
secrets_tx.commit().await?;
|
||||
let hostname = get_hostname(&mut db_handle).await?;
|
||||
|
||||
let (_, root_ca) = SslManager::init(secrets).await?.export_root_ca().await?;
|
||||
let (_, root_ca) = SslManager::init(secrets, &mut db_handle)
|
||||
.await?
|
||||
.export_root_ca()
|
||||
.await?;
|
||||
let setup_result = SetupResult {
|
||||
tor_address: format!("http://{}", tor_key.public().get_onion_address()),
|
||||
lan_address: format!(
|
||||
"https://embassy-{}.local",
|
||||
crate::hostname::derive_id(&*product_key)
|
||||
),
|
||||
lan_address: hostname.lan_address(),
|
||||
root_ca: String::from_utf8(root_ca.to_pem()?)?,
|
||||
};
|
||||
*ctx.setup_result.write().await = Some((guid, setup_result.clone()));
|
||||
@@ -222,6 +198,29 @@ pub async fn recovery_status(
|
||||
ctx.recovery_status.read().await.clone().transpose()
|
||||
}
|
||||
|
||||
/// We want to be able to get a secret, a shared private key with the frontend
|
||||
/// This way the frontend can send a secret, like the password for the setup/ recovory
|
||||
/// without knowing the password over clearnet. We use the public key shared across the network
|
||||
/// since it is fine to share the public, and encrypt against the public.
|
||||
#[command(rename = "get-secret", rpc_only, metadata(authenticated = false))]
|
||||
pub async fn get_secret(
|
||||
#[context] ctx: SetupContext,
|
||||
#[arg] pubkey: Jwk,
|
||||
) -> Result<String, RpcError> {
|
||||
let secret = ctx.update_secret().await?;
|
||||
let mut header = josekit::jwe::JweHeader::new();
|
||||
header.set_algorithm("ECDH-ES");
|
||||
header.set_content_encryption("A256GCM");
|
||||
|
||||
let encrypter = josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs
|
||||
.encrypter_from_jwk(&pubkey)
|
||||
.unwrap();
|
||||
|
||||
Ok(josekit::jwe::serialize_compact(secret.as_bytes(), &header, &encrypter).unwrap())
|
||||
// Need to encrypt from the public key sent
|
||||
// then encode via hex
|
||||
}
|
||||
|
||||
#[command(subcommands(verify_cifs))]
|
||||
pub fn cifs() -> Result<(), Error> {
|
||||
Ok(())
|
||||
@@ -269,14 +268,11 @@ pub async fn execute(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((tor_addr, root_ca)) => {
|
||||
Ok((hostname, tor_addr, root_ca)) => {
|
||||
tracing::info!("Setup Successful! Tor Address: {}", tor_addr);
|
||||
Ok(SetupResult {
|
||||
tor_address: format!("http://{}", tor_addr),
|
||||
lan_address: format!(
|
||||
"https://embassy-{}.local",
|
||||
crate::hostname::derive_id(&ctx.product_key().await?)
|
||||
),
|
||||
lan_address: hostname.lan_address(),
|
||||
root_ca: String::from_utf8(root_ca.to_pem()?)?,
|
||||
})
|
||||
}
|
||||
@@ -299,33 +295,14 @@ pub async fn complete(#[context] ctx: SetupContext) -> Result<SetupResult, Error
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
if tokio::fs::metadata(PRODUCT_KEY_PATH).await.is_err() {
|
||||
crate::hostname::set_product_key(&*ctx.product_key().await?).await?;
|
||||
} else {
|
||||
let key_on_disk = crate::hostname::get_product_key().await?;
|
||||
let key_in_cache = ctx.product_key().await?;
|
||||
if *key_in_cache != key_on_disk {
|
||||
crate::hostname::set_product_key(&*ctx.product_key().await?).await?;
|
||||
}
|
||||
}
|
||||
tokio::fs::write(
|
||||
Path::new("/embassy-data/main/product_key.txt"),
|
||||
&*ctx.product_key().await?,
|
||||
)
|
||||
.await?;
|
||||
let secrets = ctx.secret_store().await?;
|
||||
let mut db = ctx.db(&secrets).await?.handle();
|
||||
let hostname = crate::hostname::get_hostname().await?;
|
||||
let hostname = crate::hostname::get_hostname(&mut db).await?;
|
||||
let si = crate::db::DatabaseModel::new().server_info();
|
||||
si.clone()
|
||||
.id()
|
||||
.put(&mut db, &crate::hostname::get_id().await?)
|
||||
.await?;
|
||||
let id = crate::hostname::get_id(&mut db).await?;
|
||||
si.clone().id().put(&mut db, &id).await?;
|
||||
si.lan_address()
|
||||
.put(
|
||||
&mut db,
|
||||
&format!("https://{}.local", &hostname).parse().unwrap(),
|
||||
)
|
||||
.put(&mut db, &hostname.lan_address().parse().unwrap())
|
||||
.await?;
|
||||
let mut guid_file = File::create("/embassy-os/disk.guid").await?;
|
||||
guid_file.write_all(guid.as_bytes()).await?;
|
||||
@@ -341,7 +318,7 @@ pub async fn execute_inner(
|
||||
embassy_password: String,
|
||||
recovery_source: Option<BackupTargetFS>,
|
||||
recovery_password: Option<String>,
|
||||
) -> Result<(OnionAddressV3, X509), Error> {
|
||||
) -> Result<(Hostname, OnionAddressV3, X509), Error> {
|
||||
if ctx.recovery_status.read().await.is_some() {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot execute setup while in recovery!"),
|
||||
@@ -374,12 +351,11 @@ pub async fn execute_inner(
|
||||
recovery_password,
|
||||
)
|
||||
.await?;
|
||||
init(
|
||||
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
|
||||
&ctx.product_key().await?,
|
||||
)
|
||||
.await?;
|
||||
let res = (tor_addr, root_ca.clone());
|
||||
let db = init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?)
|
||||
.await?
|
||||
.db;
|
||||
let hostname = get_hostname(&mut db.handle()).await?;
|
||||
let res = (hostname.clone(), tor_addr, root_ca.clone());
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = recover_fut
|
||||
.and_then(|_| async {
|
||||
@@ -387,10 +363,7 @@ pub async fn execute_inner(
|
||||
guid,
|
||||
SetupResult {
|
||||
tor_address: format!("http://{}", tor_addr),
|
||||
lan_address: format!(
|
||||
"https://embassy-{}.local",
|
||||
crate::hostname::derive_id(&ctx.product_key().await?)
|
||||
),
|
||||
lan_address: hostname.lan_address(),
|
||||
root_ca: String::from_utf8(root_ca.to_pem()?)?,
|
||||
},
|
||||
));
|
||||
@@ -412,23 +385,19 @@ pub async fn execute_inner(
|
||||
res
|
||||
} else {
|
||||
let (tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?;
|
||||
init(
|
||||
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
|
||||
&ctx.product_key().await?,
|
||||
)
|
||||
.await?;
|
||||
let db = init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?)
|
||||
.await?
|
||||
.db;
|
||||
*ctx.setup_result.write().await = Some((
|
||||
guid,
|
||||
SetupResult {
|
||||
tor_address: format!("http://{}", tor_addr),
|
||||
lan_address: format!(
|
||||
"https://embassy-{}.local",
|
||||
crate::hostname::derive_id(&ctx.product_key().await?)
|
||||
),
|
||||
lan_address: get_hostname(&mut db.handle()).await?.lan_address(),
|
||||
root_ca: String::from_utf8(root_ca.to_pem()?)?,
|
||||
},
|
||||
));
|
||||
(tor_addr, root_ca)
|
||||
let hostname = get_hostname(&mut db.handle()).await?;
|
||||
(hostname, tor_addr, root_ca)
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
@@ -455,7 +424,8 @@ async fn fresh_setup(
|
||||
)
|
||||
.execute(&mut sqlite_pool.acquire().await?)
|
||||
.await?;
|
||||
let (_, root_ca) = SslManager::init(sqlite_pool.clone())
|
||||
let db = ctx.db(&sqlite_pool).await?;
|
||||
let (_, root_ca) = SslManager::init(sqlite_pool.clone(), &mut db.handle())
|
||||
.await?
|
||||
.export_root_ca()
|
||||
.await?;
|
||||
|
||||
@@ -24,7 +24,6 @@ use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::UpdateProgress;
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::disk::mount::filesystem::block_dev::BlockDev;
|
||||
use crate::disk::mount::filesystem::{FileSystem, ReadWrite};
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
@@ -46,26 +45,24 @@ lazy_static! {
|
||||
|
||||
/// An user/ daemon would call this to update the system to the latest version and do the updates available,
|
||||
/// and this will return something if there is an update, and in that case there will need to be a restart.
|
||||
#[command(rename = "update", display(display_update_result))]
|
||||
#[command(
|
||||
rename = "update",
|
||||
display(display_update_result),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn update_system(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(rename = "marketplace-url")] marketplace_url: Url,
|
||||
) -> Result<WithRevision<UpdateResult>, Error> {
|
||||
let noop = WithRevision {
|
||||
response: UpdateResult::NoUpdates,
|
||||
revision: None,
|
||||
};
|
||||
) -> Result<UpdateResult, Error> {
|
||||
if UPDATED.load(Ordering::SeqCst) {
|
||||
return Ok(noop);
|
||||
}
|
||||
match maybe_do_update(ctx, marketplace_url).await? {
|
||||
None => Ok(noop),
|
||||
Some(r) => Ok(WithRevision {
|
||||
response: UpdateResult::Updating,
|
||||
revision: Some(r),
|
||||
}),
|
||||
return Ok(UpdateResult::NoUpdates);
|
||||
}
|
||||
Ok(if maybe_do_update(ctx, marketplace_url).await?.is_some() {
|
||||
UpdateResult::Updating
|
||||
} else {
|
||||
UpdateResult::NoUpdates
|
||||
})
|
||||
}
|
||||
|
||||
/// What is the status of the updates?
|
||||
@@ -76,8 +73,8 @@ pub enum UpdateResult {
|
||||
Updating,
|
||||
}
|
||||
|
||||
fn display_update_result(status: WithRevision<UpdateResult>, _: &ArgMatches) {
|
||||
match status.response {
|
||||
fn display_update_result(status: UpdateResult, _: &ArgMatches) {
|
||||
match status {
|
||||
UpdateResult::Updating => {
|
||||
println!("Updating...");
|
||||
}
|
||||
@@ -190,7 +187,7 @@ async fn maybe_do_update(
|
||||
downloaded: 0,
|
||||
});
|
||||
status.save(&mut tx).await?;
|
||||
let rev = tx.commit(None).await?;
|
||||
let rev = tx.commit().await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut db = ctx.db.handle();
|
||||
|
||||
@@ -14,8 +14,9 @@ mod v0_3_0_2;
|
||||
mod v0_3_0_3;
|
||||
mod v0_3_1;
|
||||
mod v0_3_1_1;
|
||||
mod v0_3_1_2;
|
||||
|
||||
pub type Current = v0_3_1_1::Version;
|
||||
pub type Current = v0_3_1_2::Version;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
@@ -26,6 +27,7 @@ enum Version {
|
||||
V0_3_0_3(Wrapper<v0_3_0_3::Version>),
|
||||
V0_3_1(Wrapper<v0_3_1::Version>),
|
||||
V0_3_1_1(Wrapper<v0_3_1_1::Version>),
|
||||
V0_3_1_2(Wrapper<v0_3_1_2::Version>),
|
||||
Other(emver::Version),
|
||||
}
|
||||
|
||||
@@ -47,6 +49,7 @@ impl Version {
|
||||
Version::V0_3_0_3(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_1_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_1_2(Wrapper(x)) => x.semver(),
|
||||
Version::Other(x) => x.clone(),
|
||||
}
|
||||
}
|
||||
@@ -179,6 +182,7 @@ pub async fn init<Db: DbHandle>(
|
||||
Version::V0_3_0_3(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_1_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_1_2(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::Other(_) => {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot downgrade"),
|
||||
|
||||
61
backend/src/version/v0_3_1_2.rs
Normal file
61
backend/src/version/v0_3_1_2.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use emver::VersionRange;
|
||||
|
||||
use crate::hostname::{generate_id, get_hostname, sync_hostname};
|
||||
|
||||
use super::v0_3_0::V0_3_0_COMPAT;
|
||||
use super::*;
|
||||
|
||||
const V0_3_1_2: emver::Version = emver::Version::new(0, 3, 1, 2);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Version;
|
||||
#[async_trait]
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_3_1_1::Version;
|
||||
fn new() -> Self {
|
||||
Version
|
||||
}
|
||||
fn semver(&self) -> emver::Version {
|
||||
V0_3_1_2
|
||||
}
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
|
||||
let hostname = get_hostname(db).await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.hostname()
|
||||
.put(db, &Some(hostname.0))
|
||||
.await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.id()
|
||||
.put(db, &generate_id())
|
||||
.await?;
|
||||
|
||||
sync_hostname(db).await?;
|
||||
let mut ui = crate::db::DatabaseModel::new()
|
||||
.ui()
|
||||
.get(db, false)
|
||||
.await?
|
||||
.clone();
|
||||
if let serde_json::Value::Object(ref mut ui) = ui {
|
||||
ui.insert("ack-instructions".to_string(), serde_json::json!({}));
|
||||
}
|
||||
crate::db::DatabaseModel::new().ui().put(db, &ui).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
|
||||
let mut ui = crate::db::DatabaseModel::new()
|
||||
.ui()
|
||||
.get(db, false)
|
||||
.await?
|
||||
.clone();
|
||||
if let serde_json::Value::Object(ref mut ui) = ui {
|
||||
ui.remove("ack-instructions");
|
||||
}
|
||||
crate::db::DatabaseModel::new().ui().put(db, &ui).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ After=network-online.target systemd-time-wait-sync.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
ExecStart=/usr/local/bin/initialization.sh
|
||||
RemainAfterExit=true
|
||||
StandardOutput=append:/var/log/initialization.log
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Update repositories, install dependencies, do some initial configurations, set hostname, enable embassy-init, and config Tor
|
||||
set -e
|
||||
function flatline {
|
||||
echo -n "0" > /sys/class/pwm/pwmchip0/export
|
||||
sleep 0.5
|
||||
echo -n "2272727" > /sys/class/pwm/pwmchip0/pwm0/period
|
||||
echo -n "1136364" > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
|
||||
echo -n "1" > /sys/class/pwm/pwmchip0/pwm0/enable
|
||||
exit 1
|
||||
}
|
||||
|
||||
(set -e
|
||||
|
||||
# introduce start9 username and embassy as default password
|
||||
if ! awk -F: '{ print $1 }' /etc/passwd | grep start9
|
||||
@@ -146,9 +154,10 @@ systemctl disable nc-broadcast.service
|
||||
systemctl disable initialization.service
|
||||
sudo systemctl restart NetworkManager
|
||||
|
||||
sync
|
||||
|
||||
echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-embassy.conf
|
||||
|
||||
# TODO: clean out ssh host keys
|
||||
sync
|
||||
|
||||
reboot
|
||||
) || flatline
|
||||
|
||||
|
||||
174
frontend/package-lock.json
generated
174
frontend/package-lock.json
generated
@@ -29,11 +29,13 @@
|
||||
"dompurify": "^2.3.6",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"fuse.js": "^6.4.6",
|
||||
"jose": "^4.9.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"marked": "^4.0.0",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"mustache": "^4.2.0",
|
||||
"ng-qrcode": "^7.0.0",
|
||||
"node-jose": "^2.1.1",
|
||||
"patch-db-client": "file: ../../../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.5.6",
|
||||
@@ -56,6 +58,7 @@
|
||||
"@types/marked": "^4.0.3",
|
||||
"@types/mustache": "^4.1.2",
|
||||
"@types/node": "^16.9.1",
|
||||
"@types/node-jose": "^1.1.10",
|
||||
"@types/pbkdf2": "^3.1.0",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"husky": "^4.3.8",
|
||||
@@ -3721,6 +3724,15 @@
|
||||
"integrity": "sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node-jose": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-jose/-/node-jose-1.1.10.tgz",
|
||||
"integrity": "sha512-7L0ucJTugW4x/sYpQ+c5IudAwr0pFuxDVnZLpHKWpff7p1lVa3wTuNvnrzFBNeLojE+UY0cVCwNGXLxXsMIrzw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
@@ -4469,7 +4481,6 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4485,6 +4496,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/batch": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||
@@ -6127,6 +6146,11 @@
|
||||
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.15.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz",
|
||||
@@ -7860,7 +7884,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -8418,6 +8441,14 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.9.0.tgz",
|
||||
"integrity": "sha512-RgaqEOZLkVO+ViN3KkN44XJt9g7+wMveUv59sVLaTxONcUPc8ZpfqOCeLphVBZyih2dgkvZ0Ap1CNcokvY7Uyw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -8866,8 +8897,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash._baseassign": {
|
||||
"version": "3.2.0",
|
||||
@@ -9111,6 +9141,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
|
||||
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz",
|
||||
@@ -9731,7 +9766,6 @@
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
@@ -9824,6 +9858,50 @@
|
||||
"he": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-jose": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.1.1.tgz",
|
||||
"integrity": "sha512-19nyuUGShNmFmVTeqDfP6ZJCiikbcjI0Pw2kykBCH7rl8AZgSiDZK2Ww8EDaMrOSbRg6IlfIMhI5ZvCklmOhzg==",
|
||||
"dependencies": {
|
||||
"base64url": "^3.0.1",
|
||||
"buffer": "^6.0.3",
|
||||
"es6-promise": "^4.2.8",
|
||||
"lodash": "^4.17.21",
|
||||
"long": "^5.2.0",
|
||||
"node-forge": "^1.2.1",
|
||||
"pako": "^2.0.4",
|
||||
"process": "^0.11.10",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/node-jose/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-jose/node_modules/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
|
||||
@@ -11464,6 +11542,14 @@
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -17246,6 +17332,15 @@
|
||||
"integrity": "sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node-jose": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-jose/-/node-jose-1.1.10.tgz",
|
||||
"integrity": "sha512-7L0ucJTugW4x/sYpQ+c5IudAwr0pFuxDVnZLpHKWpff7p1lVa3wTuNvnrzFBNeLojE+UY0cVCwNGXLxXsMIrzw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
@@ -17863,8 +17958,12 @@
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
|
||||
},
|
||||
"batch": {
|
||||
"version": "0.6.1",
|
||||
@@ -19096,6 +19195,11 @@
|
||||
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.15.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz",
|
||||
@@ -20296,8 +20400,7 @@
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.2.0",
|
||||
@@ -20703,6 +20806,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jose": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.9.0.tgz",
|
||||
"integrity": "sha512-RgaqEOZLkVO+ViN3KkN44XJt9g7+wMveUv59sVLaTxONcUPc8ZpfqOCeLphVBZyih2dgkvZ0Ap1CNcokvY7Uyw=="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -21035,8 +21143,7 @@
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash._baseassign": {
|
||||
"version": "3.2.0",
|
||||
@@ -21239,6 +21346,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"long": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
|
||||
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz",
|
||||
@@ -21697,8 +21809,7 @@
|
||||
"node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="
|
||||
},
|
||||
"node-gyp": {
|
||||
"version": "9.1.0",
|
||||
@@ -21770,6 +21881,38 @@
|
||||
"he": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node-jose": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.1.1.tgz",
|
||||
"integrity": "sha512-19nyuUGShNmFmVTeqDfP6ZJCiikbcjI0Pw2kykBCH7rl8AZgSiDZK2Ww8EDaMrOSbRg6IlfIMhI5ZvCklmOhzg==",
|
||||
"requires": {
|
||||
"base64url": "^3.0.1",
|
||||
"buffer": "^6.0.3",
|
||||
"es6-promise": "^4.2.8",
|
||||
"lodash": "^4.17.21",
|
||||
"long": "^5.2.0",
|
||||
"node-forge": "^1.2.1",
|
||||
"pako": "^2.0.4",
|
||||
"process": "^0.11.10",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
|
||||
@@ -22889,6 +23032,11 @@
|
||||
"integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==",
|
||||
"dev": true
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
|
||||
@@ -43,11 +43,13 @@
|
||||
"dompurify": "^2.3.6",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"fuse.js": "^6.4.6",
|
||||
"jose": "^4.9.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"marked": "^4.0.0",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"mustache": "^4.2.0",
|
||||
"ng-qrcode": "^7.0.0",
|
||||
"node-jose": "^2.1.1",
|
||||
"patch-db-client": "file: ../../../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.5.6",
|
||||
@@ -70,6 +72,7 @@
|
||||
"@types/marked": "^4.0.3",
|
||||
"@types/mustache": "^4.1.2",
|
||||
"@types/node": "^16.9.1",
|
||||
"@types/node-jose": "^1.1.10",
|
||||
"@types/pbkdf2": "^3.1.0",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"husky": "^4.3.8",
|
||||
|
||||
@@ -156,7 +156,6 @@ export class HomePage {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
|
||||
@@ -1,46 +1,61 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpService } from '@start9labs/shared'
|
||||
import {
|
||||
HttpService,
|
||||
isRpcError,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService, GetErrorRes } from './api.service'
|
||||
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
constructor(private readonly http: HttpService) {
|
||||
super()
|
||||
}
|
||||
export class LiveApiService implements ApiService {
|
||||
constructor(private readonly http: HttpService) {}
|
||||
|
||||
getError(): Promise<GetErrorRes> {
|
||||
return this.http.rpcRequest<GetErrorRes>({
|
||||
return this.rpcRequest<GetErrorRes>({
|
||||
method: 'diagnostic.error',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
restart(): Promise<void> {
|
||||
return this.http.rpcRequest<void>({
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.restart',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
forgetDrive(): Promise<void> {
|
||||
return this.http.rpcRequest<void>({
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.forget',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
repairDisk(): Promise<void> {
|
||||
return this.http.rpcRequest<void>({
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.repair',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
getLogs(params: ServerLogsReq): Promise<LogsRes> {
|
||||
return this.http.rpcRequest<LogsRes>({
|
||||
return this.rpcRequest<LogsRes>({
|
||||
method: 'diagnostic.logs',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const res = await this.http.rpcRequest<T>(opts)
|
||||
|
||||
const rpcRes = res.body
|
||||
|
||||
if (isRpcError(rpcRes)) {
|
||||
throw new RpcError(rpcRes.error)
|
||||
}
|
||||
|
||||
return rpcRes.result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@ import { ApiService, GetErrorRes } from './api.service'
|
||||
import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared'
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService extends ApiService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
export class MockApiService implements ApiService {
|
||||
async getError(): Promise<GetErrorRes> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
|
||||
@@ -50,7 +50,6 @@ export class AdditionalComponent {
|
||||
{
|
||||
text: 'Ok',
|
||||
handler: (version: string) => this.version.emit(version),
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,45 +1,32 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||
import { NavGuard, RecoveryNavGuard } from './guards/nav-guard'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: '/product-key', pathMatch: 'full' },
|
||||
{
|
||||
path: 'product-key',
|
||||
loadChildren: () =>
|
||||
import('./pages/product-key/product-key.module').then(
|
||||
m => m.ProductKeyPageModule,
|
||||
),
|
||||
},
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home.module').then(m => m.HomePageModule),
|
||||
canActivate: [NavGuard],
|
||||
},
|
||||
{
|
||||
path: 'recover',
|
||||
loadChildren: () =>
|
||||
import('./pages/recover/recover.module').then(m => m.RecoverPageModule),
|
||||
canActivate: [RecoveryNavGuard],
|
||||
},
|
||||
{
|
||||
path: 'embassy',
|
||||
loadChildren: () =>
|
||||
import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule),
|
||||
canActivate: [NavGuard],
|
||||
},
|
||||
{
|
||||
path: 'loading',
|
||||
loadChildren: () =>
|
||||
import('./pages/loading/loading.module').then(m => m.LoadingPageModule),
|
||||
canActivate: [NavGuard],
|
||||
},
|
||||
{
|
||||
path: 'success',
|
||||
loadChildren: () =>
|
||||
import('./pages/success/success.module').then(m => m.SuccessPageModule),
|
||||
canActivate: [NavGuard],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { StateService } from './services/state.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -14,21 +13,12 @@ export class AppComponent {
|
||||
private readonly apiService: ApiService,
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const status = await this.apiService.getStatus()
|
||||
if (status.migrating || status['product-key']) {
|
||||
this.stateService.hasProductKey = true
|
||||
this.stateService.isMigrating = status.migrating
|
||||
await this.navCtrl.navigateForward(`/product-key`)
|
||||
} else {
|
||||
this.stateService.hasProductKey = false
|
||||
this.stateService.isMigrating = false
|
||||
await this.navCtrl.navigateForward(`/recover`)
|
||||
}
|
||||
const { migrating } = await this.apiService.getStatus()
|
||||
await this.navCtrl.navigateForward(migrating ? '/loading' : '/home')
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ErrorHandler, NgModule } from '@angular/core'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
@@ -15,8 +15,6 @@ import { AppRoutingModule } from './app-routing.module'
|
||||
import { SuccessPageModule } from './pages/success/success.module'
|
||||
import { HomePageModule } from './pages/home/home.module'
|
||||
import { LoadingPageModule } from './pages/loading/loading.module'
|
||||
import { ProdKeyModalModule } from './modals/prod-key-modal/prod-key-modal.module'
|
||||
import { ProductKeyPageModule } from './pages/product-key/product-key.module'
|
||||
import { RecoverPageModule } from './pages/recover/recover.module'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
|
||||
@@ -35,8 +33,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
SuccessPageModule,
|
||||
HomePageModule,
|
||||
LoadingPageModule,
|
||||
ProdKeyModalModule,
|
||||
ProductKeyPageModule,
|
||||
RecoverPageModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { CanActivate, Router } from '@angular/router'
|
||||
import { RPCEncryptedService } from '../services/rpc-encrypted.service'
|
||||
import { StateService } from '../services/state.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NavGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
if (this.encrypted.productKey) {
|
||||
return true
|
||||
} else {
|
||||
this.router.navigateByUrl('product-key')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RecoveryNavGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
if (this.encrypted.productKey || !this.stateService.hasProductKey) {
|
||||
return true
|
||||
} else {
|
||||
this.router.navigateByUrl('product-key')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, ModalController } from '@ionic/angular'
|
||||
import {
|
||||
DiskInfo,
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.service'
|
||||
@@ -15,7 +14,7 @@ import * as argon2 from '@start9labs/argon2'
|
||||
export class PasswordPage {
|
||||
@ViewChild('focusInput') elem?: IonInput
|
||||
@Input() target?: CifsBackupTarget | DiskBackupTarget
|
||||
@Input() storageDrive?: DiskInfo
|
||||
@Input() storageDrive = false
|
||||
|
||||
pwError = ''
|
||||
password = ''
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ProdKeyModal } from './prod-key-modal.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ProdKeyModal,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [
|
||||
ProdKeyModal,
|
||||
],
|
||||
})
|
||||
export class ProdKeyModalModule { }
|
||||
@@ -1,41 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Enter Product Key
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<form (ngSubmit)="verifyProductKey()">
|
||||
<div style="padding: 8px 24px;">
|
||||
<div style="padding-bottom: 16px;">
|
||||
<p>Enter your 0.2.x Product Key to establish an encrypted connection with your new Embassy.</p>
|
||||
</div>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
#focusInput
|
||||
[(ngModel)]="productKey"
|
||||
placeholder="Enter Product Key"
|
||||
maxlength="12"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<div style="height: 16px;">
|
||||
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" style="display: none" />
|
||||
</form>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" (click)="verifyProductKey()">
|
||||
Submit
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
|
||||
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
|
||||
|
||||
@Component({
|
||||
selector: 'prod-key-modal',
|
||||
templateUrl: 'prod-key-modal.page.html',
|
||||
styleUrls: ['prod-key-modal.page.scss'],
|
||||
})
|
||||
export class ProdKeyModal {
|
||||
@ViewChild('focusInput') elem?: IonInput
|
||||
@Input() target!: DiskBackupTarget
|
||||
|
||||
error = ''
|
||||
productKey = ''
|
||||
unmasked = false
|
||||
|
||||
constructor(
|
||||
private readonly modalController: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.elem?.setFocus(), 400)
|
||||
}
|
||||
|
||||
async verifyProductKey() {
|
||||
if (!this.productKey || !this.target.logicalname) return
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Verifying Product Key',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.apiService.set02XDrive(this.target.logicalname)
|
||||
this.encrypted.productKey = this.productKey
|
||||
await this.apiService.verifyProductKey()
|
||||
this.modalController.dismiss({ productKey: this.productKey }, 'success')
|
||||
} catch (e) {
|
||||
this.encrypted.productKey = undefined
|
||||
this.error = 'Invalid Product Key'
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -64,12 +64,7 @@ export class EmbassyPage {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: `One or more devices you connected had to be reconfigured to support the current hardware platform. Please unplug and replug the following device(s), then refresh the page:<br> ${list}`,
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
text: 'OK',
|
||||
},
|
||||
],
|
||||
buttons: ['OK'],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
@@ -95,40 +90,45 @@ export class EmbassyPage {
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(drive, this.stateService.recoveryPassword)
|
||||
this.setupEmbassy(
|
||||
drive.logicalname,
|
||||
this.stateService.recoveryPassword,
|
||||
)
|
||||
} else {
|
||||
this.presentModalPassword(drive)
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
}
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(drive, this.stateService.recoveryPassword)
|
||||
this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword)
|
||||
} else {
|
||||
this.presentModalPassword(drive)
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalPassword(drive: DiskInfo): Promise<void> {
|
||||
private async presentModalPassword(logicalname: string): Promise<void> {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: {
|
||||
storageDrive: drive,
|
||||
storageDrive: true,
|
||||
},
|
||||
})
|
||||
modal.onDidDismiss().then(async ret => {
|
||||
if (!ret.data || !ret.data.password) return
|
||||
this.setupEmbassy(drive, ret.data.password)
|
||||
this.setupEmbassy(logicalname, ret.data.password)
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async setupEmbassy(drive: DiskInfo, password: string): Promise<void> {
|
||||
private async setupEmbassy(
|
||||
logicalname: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Initializing data drive. This could take a while...',
|
||||
})
|
||||
@@ -136,7 +136,7 @@ export class EmbassyPage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.stateService.setupEmbassy(drive.logicalname, password)
|
||||
await this.stateService.setupEmbassy(logicalname, password)
|
||||
if (!!this.stateService.recoverySource) {
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} else {
|
||||
|
||||
@@ -4,9 +4,8 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { HomePage } from './home.page'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
|
||||
import { HomePageRoutingModule } from './home-routing.module'
|
||||
|
||||
import { SwiperModule } from 'swiper/angular'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -15,7 +14,8 @@ import { HomePageRoutingModule } from './home-routing.module'
|
||||
IonicModule,
|
||||
HomePageRoutingModule,
|
||||
PasswordPageModule,
|
||||
SwiperModule,
|
||||
],
|
||||
declarations: [HomePage],
|
||||
})
|
||||
export class HomePageModule { }
|
||||
export class HomePageModule {}
|
||||
|
||||
@@ -2,36 +2,78 @@
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px;" />
|
||||
<div style="padding-bottom: 32px">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px" />
|
||||
</div>
|
||||
|
||||
<ion-card color="dark">
|
||||
<ion-card-content class="ion-margin">
|
||||
<!-- fresh -->
|
||||
<ion-card
|
||||
<ion-card-header>
|
||||
<ion-button
|
||||
*ngIf="swiper?.activeIndex === 1"
|
||||
class="back-button"
|
||||
fill="clear"
|
||||
color="light"
|
||||
(click)="previous()"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-card-title>
|
||||
Embassy Setup
|
||||
<span *ngIf="swiper?.activeIndex === 1"> (recover)</span>
|
||||
</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content class="ion-margin-bottom">
|
||||
<swiper (swiper)="setSwiperInstance($event)">
|
||||
<ng-template swiperSlide>
|
||||
<ion-item
|
||||
button
|
||||
[disabled]="error"
|
||||
detail="true"
|
||||
routerLink="/embassy"
|
||||
color="light"
|
||||
style="text-align: center; background-color: #00919b !important; height: 160px; margin-bottom: 20px; box-shadow: 4px 4px 16px var(--ion-color-light);"
|
||||
>
|
||||
<ion-card-header>
|
||||
<ion-card-title style="font-size: 40px;">Start Fresh</ion-card-title>
|
||||
<ion-card-subtitle>Get started with a brand new Embassy</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
|
||||
<!-- recover -->
|
||||
</ion-card>
|
||||
<ion-card
|
||||
routerLink="/recover"
|
||||
color="light"
|
||||
style="text-align: center; background-color: #bf5900 !important; height: 160px; box-shadow: 4px 4px 16px var(--ion-color-light);"
|
||||
<ion-icon color="dark" slot="start" name="add"></ion-icon>
|
||||
<ion-label>
|
||||
<h2><ion-text color="success">Start Fresh</ion-text></h2>
|
||||
<p>Get started with a brand new Embassy</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
button
|
||||
[disabled]="error"
|
||||
detail="true"
|
||||
lines="none"
|
||||
(click)="next()"
|
||||
>
|
||||
<ion-card-header>
|
||||
<ion-card-title style="font-size: 40px;">Recover</ion-card-title>
|
||||
<ion-card-subtitle>Restore from backup or recover an old Embassy</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
</ion-card>
|
||||
<ion-icon color="dark" slot="start" name="reload"></ion-icon>
|
||||
<ion-label>
|
||||
<h2><ion-text color="danger">Recover</ion-text></h2>
|
||||
<p>
|
||||
Restore from backup or use an existing Embassy data drive
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
<ng-template swiperSlide>
|
||||
<ion-item button detail="true" routerLink="/recover">
|
||||
<ion-icon color="dark" slot="start" name="save"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<ion-text color="warning">Restore From Backup</ion-text>
|
||||
</h2>
|
||||
<p>Recover an Embassy from encrypted backup</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button detail="true" lines="none" (click)="import()">
|
||||
<ion-icon color="dark" slot="start" name="cube"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<ion-text color="primary">Use Existing Drive</ion-text>
|
||||
</h2>
|
||||
<p>Attach and use a valid Embassy data drive</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</swiper>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 24px;
|
||||
z-index: 1000000;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--background: var(--ion-color-medium);
|
||||
--color: var(--ion-color-dark);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -1,9 +1,112 @@
|
||||
import { Component } from '@angular/core'
|
||||
import {
|
||||
AlertController,
|
||||
IonicSlides,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import { PasswordPage } from 'src/app/modals/password/password.page'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import SwiperCore, { Swiper } from 'swiper'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
SwiperCore.use([IonicSlides])
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: 'home.page.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
})
|
||||
export class HomePage { }
|
||||
export class HomePage {
|
||||
swiper?: Swiper
|
||||
guid?: string | null
|
||||
error = false
|
||||
|
||||
constructor(
|
||||
private readonly unencrypted: ApiService,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly stateService: StateService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly errToastService: ErrorToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
this.encrypted.secret = await this.unencrypted.getSecret()
|
||||
const { disks } = await this.unencrypted.getDrives()
|
||||
this.guid = disks.find(d => !!d.guid)?.guid
|
||||
} catch (e: any) {
|
||||
this.error = true
|
||||
this.errToastService.present(e)
|
||||
}
|
||||
}
|
||||
|
||||
async ionViewDidEnter() {
|
||||
if (this.swiper) {
|
||||
this.swiper.allowTouchMove = false
|
||||
}
|
||||
}
|
||||
|
||||
setSwiperInstance(swiper: any) {
|
||||
this.swiper = swiper
|
||||
}
|
||||
|
||||
next() {
|
||||
this.swiper?.slideNext(500)
|
||||
}
|
||||
|
||||
previous() {
|
||||
this.swiper?.slidePrev(500)
|
||||
}
|
||||
|
||||
async import() {
|
||||
if (this.guid) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { storageDrive: true },
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.password) {
|
||||
this.importDrive(res.data.password)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Drive Not Found',
|
||||
message:
|
||||
'Please make sure the drive is a valid Embassy data drive (not a backup) and is firmly connected, then refresh the page.',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
private async importDrive(password: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Importing Drive',
|
||||
})
|
||||
await loader.present()
|
||||
try {
|
||||
await this.stateService.importDrive(this.guid!, password)
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
} catch (e: any) {
|
||||
this.errToastService.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decodeHex(hex: string) {
|
||||
let str = ''
|
||||
for (let n = 0; n < hex.length; n += 2) {
|
||||
str += String.fromCharCode(parseInt(hex.substring(n, 2), 16))
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { ProductKeyPage } from './product-key.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProductKeyPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProductKeyPageRoutingModule { }
|
||||
@@ -1,19 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ProductKeyPage } from './product-key.page'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
import { ProductKeyPageRoutingModule } from './product-key-routing.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ProductKeyPageRoutingModule,
|
||||
PasswordPageModule,
|
||||
],
|
||||
declarations: [ProductKeyPage],
|
||||
})
|
||||
export class ProductKeyPageModule { }
|
||||
@@ -1,43 +0,0 @@
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px;" />
|
||||
</div>
|
||||
|
||||
<ion-card color="dark">
|
||||
<ion-card-header style="padding-bottom: 8px;">
|
||||
<ion-card-title>Product Key</ion-card-title>
|
||||
<ion-card-subtitle>Enter your product key to establish an encrypted connection with your Embassy</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<form (submit)="submit()" style="margin-bottom: 12px;">
|
||||
<ion-item-group class="ion-padding-bottom">
|
||||
<ion-item color="dark">
|
||||
<ion-icon slot="start" name="key-outline" style="margin-right: 16px;"></ion-icon>
|
||||
<ion-input
|
||||
#focusInput
|
||||
name="productKey"
|
||||
[(ngModel)]="productKey"
|
||||
(ionChange)="error = ''"
|
||||
maxlength="12"
|
||||
>
|
||||
</ion-input>
|
||||
</ion-item>
|
||||
<div class="ion-text-left">
|
||||
<p *ngIf="error" style="padding-top: 4px"><ion-text color="danger">{{ error }}</ion-text></p>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
<ion-button type="submit" color="light" class="claim-button">
|
||||
Submit
|
||||
</ion-button>
|
||||
</form>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
@@ -1,5 +0,0 @@
|
||||
ion-item {
|
||||
--border-style: solid;
|
||||
--border-width: 1px;
|
||||
--border-color: var(--ion-color-medium);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { IonInput, LoadingController, NavController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-key',
|
||||
templateUrl: 'product-key.page.html',
|
||||
styleUrls: ['product-key.page.scss'],
|
||||
})
|
||||
export class ProductKeyPage {
|
||||
@ViewChild('focusInput') elem?: IonInput
|
||||
productKey = ''
|
||||
error = ''
|
||||
|
||||
constructor(
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly stateService: StateService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
setTimeout(() => this.elem?.setFocus(), 400)
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.productKey) return (this.error = 'Must enter product key')
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Verifying Product Key',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
this.encrypted.productKey = this.productKey
|
||||
await this.apiService.verifyProductKey()
|
||||
if (this.stateService.isMigrating) {
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} else {
|
||||
await this.navCtrl.navigateForward(`/home`)
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = 'Invalid Product Key'
|
||||
this.encrypted.productKey = undefined
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { FormsModule } from '@angular/forms'
|
||||
import { UnitConversionPipesModule } from '@start9labs/shared'
|
||||
import { DriveStatusComponent, RecoverPage } from './recover.page'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
import { ProdKeyModalModule } from '../../modals/prod-key-modal/prod-key-modal.module'
|
||||
import { RecoverPageRoutingModule } from './recover-routing.module'
|
||||
import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
|
||||
|
||||
@@ -17,7 +16,6 @@ import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
|
||||
IonicModule,
|
||||
RecoverPageRoutingModule,
|
||||
PasswordPageModule,
|
||||
ProdKeyModalModule,
|
||||
UnitConversionPipesModule,
|
||||
CifsModalModule,
|
||||
],
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import {
|
||||
AlertController,
|
||||
IonicSafeString,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import { AlertController, ModalController, NavController } from '@ionic/angular'
|
||||
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
|
||||
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-recover',
|
||||
@@ -21,7 +14,6 @@ import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
|
||||
export class RecoverPage {
|
||||
loading = true
|
||||
mappedDrives: MappedDisk[] = []
|
||||
hasShownGuidAlert = false
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
@@ -29,8 +21,7 @@ export class RecoverPage {
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly modalController: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
private readonly errToastService: ErrorToastService,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
@@ -44,10 +35,7 @@ export class RecoverPage {
|
||||
}
|
||||
|
||||
driveClickable(mapped: MappedDisk) {
|
||||
return (
|
||||
mapped.drive['embassy-os']?.full &&
|
||||
(this.stateService.hasProductKey || mapped.is02x)
|
||||
)
|
||||
return mapped.drive['embassy-os']?.full
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
@@ -89,50 +77,8 @@ export class RecoverPage {
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
const importableDrive = disks.find(d => !!d.guid)
|
||||
if (
|
||||
!!importableDrive &&
|
||||
this.stateService.hasProductKey &&
|
||||
!this.hasShownGuidAlert
|
||||
) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Embassy Data Drive Detected',
|
||||
message: new IonicSafeString(
|
||||
`<strong>${importableDrive.vendor || 'Unknown Vendor'} - ${
|
||||
importableDrive.model || 'Unknown Model'
|
||||
}</strong> contains Embassy data.
|
||||
<p>To use this drive and its data, select <strong>"USE DRIVE"</strong>. This will complete the setup process.
|
||||
<p><strong style="color:red">Important!</strong><br><br>
|
||||
If you are trying to restore from a backup or update from 0.2.x, <strong>DO NOT</strong> select "USE DRIVE". Instead, select <strong>"CANCEL"</strong> and follow instructions.`,
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
text: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Use Drive',
|
||||
handler: async () => {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { storageDrive: importableDrive },
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.password) {
|
||||
this.importDrive(importableDrive.guid!, res.data.password)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
this.hasShownGuidAlert = true
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
this.errToastService.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -165,7 +111,6 @@ export class RecoverPage {
|
||||
|
||||
if (!logicalname) return
|
||||
|
||||
if (this.stateService.hasProductKey) {
|
||||
if (is02x) {
|
||||
this.selectRecoverySource(logicalname)
|
||||
} else {
|
||||
@@ -181,50 +126,6 @@ export class RecoverPage {
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
// if no product key, it means they are an upgrade kit user
|
||||
} else {
|
||||
if (!is02x) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Error',
|
||||
message:
|
||||
'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.',
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
text: 'OK',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
const modal = await this.modalController.create({
|
||||
component: ProdKeyModal,
|
||||
componentProps: { target },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data?.productKey) {
|
||||
this.selectRecoverySource(logicalname)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async importDrive(guid: string, password: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Importing Drive',
|
||||
})
|
||||
await loader.present()
|
||||
try {
|
||||
await this.stateService.importDrive(guid, password)
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async selectRecoverySource(logicalname: string, password?: string) {
|
||||
|
||||
@@ -9,51 +9,48 @@
|
||||
name="checkmark-circle-outline"
|
||||
></ion-icon>
|
||||
<ion-card-title>Setup Complete</ion-card-title>
|
||||
<ion-card-subtitle
|
||||
><b
|
||||
>You have successully claimed your Embassy!</b
|
||||
></ion-card-subtitle
|
||||
>
|
||||
<br />
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<br />
|
||||
<ng-template
|
||||
[ngIf]="recoverySource && recoverySource.type === 'disk'"
|
||||
<br />
|
||||
<h2
|
||||
*ngIf="recoverySource && recoverySource.type === 'disk'"
|
||||
class="ion-padding-bottom"
|
||||
>
|
||||
<h2>You can now safely unplug your backup drive.</h2>
|
||||
</ng-template>
|
||||
<h2>
|
||||
You have successully claimed your Embassy! You can now access your
|
||||
device using the methods below.
|
||||
You can now safely unplug your backup drive.
|
||||
</h2>
|
||||
<h2 style="font-weight: bold">
|
||||
Access your Embassy using the methods below. You should
|
||||
<a (click)="download()" class="inline">
|
||||
download this page <ion-icon name="download-outline"></ion-icon>
|
||||
</a>
|
||||
for your records.
|
||||
</h2>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
<b>Note:</b> embassy.local was for setup purposes only, it will no
|
||||
longer work.
|
||||
</p>
|
||||
<div class="line"></div>
|
||||
|
||||
<!-- LAN Instructions -->
|
||||
<div (click)="toggleLan()" class="toggle-label">
|
||||
<h2>From Home (LAN)</h2>
|
||||
<ion-icon
|
||||
name="chevron-down-outline"
|
||||
[ngStyle]="{
|
||||
'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
|
||||
'transition': 'transform 0.4s ease-out'
|
||||
}"
|
||||
></ion-icon>
|
||||
</div>
|
||||
<h1><b>From Home (LAN)</b></h1>
|
||||
|
||||
<div
|
||||
[ngStyle]="{
|
||||
'overflow' : 'hidden',
|
||||
'max-height': lanOpen ? '500px' : '0px',
|
||||
'transition': 'max-height 0.4s ease-out'
|
||||
}"
|
||||
>
|
||||
<div class="ion-padding ion-text-start">
|
||||
<p>
|
||||
Visit the address below when you are conncted to the same WiFi
|
||||
or Local Area Network (LAN) as your Embassy:
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
<b>Note:</b> embassy.local was for setup purposes only, it will
|
||||
no longer work.
|
||||
</p>
|
||||
|
||||
<ion-item
|
||||
lines="none"
|
||||
color="dark"
|
||||
@@ -66,6 +63,14 @@
|
||||
></code
|
||||
>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
[href]="lanAddress"
|
||||
target="_blank"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
@@ -84,45 +89,25 @@
|
||||
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="inline"
|
||||
>
|
||||
<b>download and trust</b>
|
||||
follow the instructions
|
||||
<ion-icon name="open-outline"></ion-icon>
|
||||
</a>
|
||||
your Embassy's Root Certificate Authority.
|
||||
to downlaod and trust your Embassy's Root Certificate Authority.
|
||||
</p>
|
||||
|
||||
<ion-button
|
||||
style="margin-top: 24px; margin-bottom: 24px"
|
||||
color="light"
|
||||
(click)="installCert()"
|
||||
>
|
||||
<ion-button style="margin-top: 24px" (click)="installCert()">
|
||||
Download Root CA
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
|
||||
<br />
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
|
||||
<!-- Tor Instructions -->
|
||||
<div (click)="toggleTor()" class="toggle-label">
|
||||
<h2>On The Go (Tor)</h2>
|
||||
<ion-icon
|
||||
name="chevron-down-outline"
|
||||
[ngStyle]="{
|
||||
'transform': torOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
|
||||
'transition': 'transform 0.4s ease-out'
|
||||
}"
|
||||
></ion-icon>
|
||||
</div>
|
||||
<h1><b>On The Go (Tor)</b></h1>
|
||||
|
||||
<div
|
||||
[ngStyle]="{
|
||||
'overflow' : 'hidden',
|
||||
'max-height': torOpen ? '500px' : '0px',
|
||||
'transition': 'max-height 0.4s ease-out'
|
||||
}"
|
||||
>
|
||||
<div class="ion-padding ion-text-start">
|
||||
<p>Visit the address below when you are away from home:</p>
|
||||
|
||||
@@ -154,29 +139,13 @@
|
||||
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="inline"
|
||||
>
|
||||
<b>Tor-enabled browser</b> </a
|
||||
Tor-enabled browser
|
||||
<ion-icon name="open-outline"></ion-icon> </a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="ion-text-center ion-padding-top">
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
color="primary"
|
||||
strong
|
||||
(click)="download()"
|
||||
>
|
||||
Download this page
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
<br />
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
|
||||
@@ -4,26 +4,12 @@ p {
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
padding: 24px 0 8px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
* {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
text-align: right;
|
||||
font-size: 24px;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.line {
|
||||
margin-bottom: 48px;
|
||||
padding-bottom: 48px;
|
||||
border-bottom: solid 1px;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import { StateService } from 'src/app/services/state.service'
|
||||
})
|
||||
export class SuccessPage {
|
||||
@Output() onDownload = new EventEmitter()
|
||||
torOpen = false
|
||||
lanOpen = false
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
@@ -69,14 +67,6 @@ export class SuccessPage {
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
toggleTor() {
|
||||
this.torOpen = !this.torOpen
|
||||
}
|
||||
|
||||
toggleLan() {
|
||||
this.lanOpen = !this.lanOpen
|
||||
}
|
||||
|
||||
installCert() {
|
||||
this.document.getElementById('install-cert')?.click()
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
export abstract class ApiService {
|
||||
// unencrypted
|
||||
abstract getStatus(): Promise<GetStatusRes> // setup.status
|
||||
abstract getSecret(): Promise<string> // setup.get-secret
|
||||
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
|
||||
abstract set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
|
||||
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
|
||||
|
||||
// encrypted
|
||||
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
|
||||
abstract verifyProductKey(): Promise<void> // echo - throws error if invalid
|
||||
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
|
||||
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
|
||||
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
|
||||
}
|
||||
|
||||
export type GetStatusRes = {
|
||||
'product-key': boolean
|
||||
migrating: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpService } from '@start9labs/shared'
|
||||
import {
|
||||
HttpService,
|
||||
isRpcError,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
ApiService,
|
||||
CifsRecoverySource,
|
||||
@@ -13,43 +18,71 @@ import {
|
||||
SetupEmbassyRes,
|
||||
} from './api.service'
|
||||
import { RPCEncryptedService } from '../rpc-encrypted.service'
|
||||
import * as jose from 'node-jose'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LiveApiService extends ApiService {
|
||||
export class LiveApiService implements ApiService {
|
||||
constructor(
|
||||
private readonly unencrypted: HttpService,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
) {}
|
||||
|
||||
// ** UNENCRYPTED **
|
||||
|
||||
async getStatus() {
|
||||
return this.unencrypted.rpcRequest<GetStatusRes>({
|
||||
return this.rpcRequest<GetStatusRes>({
|
||||
method: 'setup.status',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to update the secret, which means that we will call in clearnet the
|
||||
* getSecret, and all the information is never in the clear, and only public
|
||||
* information is sent across the network. We don't want to expose that we do
|
||||
* this wil all public/private key, which means that there is no information loss
|
||||
* through the network.
|
||||
*/
|
||||
async getSecret() {
|
||||
const keystore = jose.JWK.createKeyStore()
|
||||
const key = await keystore.generate('EC', 'P-256')
|
||||
// const { privateKey, publicKey } =
|
||||
|
||||
// jose.generateKeyPair('ECDH-ES', {
|
||||
// extractable: true,
|
||||
// })
|
||||
console.log({ publicKey: key.toJSON() })
|
||||
const response: string = await this.rpcRequest({
|
||||
method: 'setup.get-secret',
|
||||
params: { pubkey: key.toJSON() },
|
||||
})
|
||||
|
||||
// const { plaintext } = await jose.compactDecrypt(response, privateKey)
|
||||
const decrypted = await jose.JWE.createDecrypt(key).decrypt(response)
|
||||
const decoded = new TextDecoder().decode(decrypted.plaintext)
|
||||
console.log({ decoded })
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
return this.unencrypted.rpcRequest<DiskListResponse>({
|
||||
return this.rpcRequest<DiskListResponse>({
|
||||
method: 'setup.disk.list',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async set02XDrive(logicalname: string) {
|
||||
return this.unencrypted.rpcRequest<void>({
|
||||
return this.rpcRequest<void>({
|
||||
method: 'setup.recovery.v2.set',
|
||||
params: { logicalname },
|
||||
})
|
||||
}
|
||||
|
||||
async getRecoveryStatus() {
|
||||
return this.unencrypted.rpcRequest<RecoveryStatusRes>({
|
||||
return this.rpcRequest<RecoveryStatusRes>({
|
||||
method: 'setup.recovery.status',
|
||||
params: {},
|
||||
})
|
||||
@@ -65,13 +98,6 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async verifyProductKey() {
|
||||
return this.encrypted.rpcRequest<void>({
|
||||
method: 'echo',
|
||||
params: { message: 'hello' },
|
||||
})
|
||||
}
|
||||
|
||||
async importDrive(params: ImportDriveReq) {
|
||||
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.attach',
|
||||
@@ -113,6 +139,18 @@ export class LiveApiService extends ApiService {
|
||||
'root-ca': btoa(res['root-ca']),
|
||||
}
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const res = await this.unencrypted.rpcRequest<T>(opts)
|
||||
|
||||
const rpcRes = res.body
|
||||
|
||||
if (isRpcError(rpcRes)) {
|
||||
throw new RpcError(rpcRes.error)
|
||||
}
|
||||
|
||||
return rpcRes.result
|
||||
}
|
||||
}
|
||||
|
||||
function isCifsSource(
|
||||
|
||||
@@ -12,21 +12,29 @@ let tries = 0
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MockApiService extends ApiService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
export class MockApiService implements ApiService {
|
||||
// ** UNENCRYPTED **
|
||||
|
||||
async getStatus() {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
'product-key': true,
|
||||
migrating: false,
|
||||
}
|
||||
}
|
||||
|
||||
async getSecret() {
|
||||
await pauseFor(1000)
|
||||
|
||||
const ascii = 'thisisasecret'
|
||||
|
||||
const arr1 = []
|
||||
for (let n = 0, l = ascii.length; n < l; n++) {
|
||||
var hex = Number(ascii.charCodeAt(n)).toString(16)
|
||||
arr1.push(hex)
|
||||
}
|
||||
return arr1.join('')
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
@@ -84,11 +92,6 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async verifyProductKey() {
|
||||
await pauseFor(1000)
|
||||
return
|
||||
}
|
||||
|
||||
async importDrive(params: ImportDriveReq) {
|
||||
await pauseFor(3000)
|
||||
return setupRes
|
||||
|
||||
@@ -15,13 +15,13 @@ import {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RPCEncryptedService {
|
||||
productKey?: string
|
||||
secret?: string
|
||||
|
||||
constructor(private readonly http: HttpService) {}
|
||||
|
||||
async rpcRequest<T>(opts: Omit<RPCOptions, 'timeout'>): Promise<T> {
|
||||
const encryptedBody = await AES_CTR.encryptPbkdf2(
|
||||
this.productKey || '',
|
||||
this.secret || '',
|
||||
encodeUtf8(JSON.stringify(opts)),
|
||||
)
|
||||
|
||||
@@ -36,7 +36,11 @@ export class RPCEncryptedService {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(body => AES_CTR.decryptPbkdf2(this.productKey || '', body))
|
||||
.then(res => AES_CTR.decryptPbkdf2(this.secret || '', res.body))
|
||||
.then(x => {
|
||||
console.log(`Network: ${x}`)
|
||||
return x
|
||||
})
|
||||
.then(res => JSON.parse(res))
|
||||
.catch(e => {
|
||||
if (!e.status && !e.statusText) {
|
||||
|
||||
@@ -11,9 +11,6 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StateService {
|
||||
hasProductKey = false
|
||||
isMigrating = false
|
||||
|
||||
polling = false
|
||||
embassyLoaded = false
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ ion-content {
|
||||
ion-grid {
|
||||
padding-top: 32px;
|
||||
height: 100%;
|
||||
max-width: 600px;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
ion-row {
|
||||
@@ -47,6 +47,8 @@ ion-row {
|
||||
|
||||
ion-item {
|
||||
--color: var(--ion-color-light);
|
||||
--highlight-color-valid: transparent;
|
||||
--highlight-color-invalid: transparent;
|
||||
}
|
||||
|
||||
ion-toolbar {
|
||||
@@ -61,13 +63,6 @@ ion-avatar {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--highlight-color-valid: transparent;
|
||||
--highlight-color-invalid: transparent;
|
||||
|
||||
--border-radius: 4px;
|
||||
}
|
||||
|
||||
ion-card-title {
|
||||
margin: 16px 0;
|
||||
font-family: 'Montserrat';
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { RpcErrorDetails } from '../types/rpc-error-details'
|
||||
import { RPCErrorDetails } from '../types/rpc.types'
|
||||
|
||||
export class RpcError<T> {
|
||||
export class RpcError {
|
||||
readonly code = this.error.code
|
||||
readonly message = this.getMessage()
|
||||
readonly revision = this.getRevision()
|
||||
|
||||
constructor(private readonly error: RpcErrorDetails<T>) {}
|
||||
constructor(private readonly error: RPCErrorDetails) {}
|
||||
|
||||
private getMessage(): string {
|
||||
if (typeof this.error.data === 'string') {
|
||||
@@ -16,10 +15,4 @@ export class RpcError<T> {
|
||||
? `${this.error.message}\n\n${this.error.data.details}`
|
||||
: this.error.message
|
||||
}
|
||||
|
||||
private getRevision(): T | null {
|
||||
return typeof this.error.data === 'string'
|
||||
? null
|
||||
: this.error.data?.revision || null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,15 @@ export class MarkdownComponent {
|
||||
@Input() content!: string | Observable<string>
|
||||
@Input() title!: string
|
||||
|
||||
private readonly data$ = defer(() =>
|
||||
readonly content$ = defer(() =>
|
||||
isObservable(this.content) ? this.content : of(this.content),
|
||||
).pipe(share())
|
||||
|
||||
readonly error$ = this.data$.pipe(
|
||||
readonly error$ = this.content$.pipe(
|
||||
ignoreElements(),
|
||||
catchError(e => of(getErrorMessage(e))),
|
||||
)
|
||||
|
||||
readonly content$ = this.data$.pipe(catchError(() => of([])))
|
||||
|
||||
constructor(private readonly modalCtrl: ModalController) {}
|
||||
|
||||
async dismiss() {
|
||||
|
||||
@@ -6,11 +6,22 @@ import * as DOMPurify from 'dompurify'
|
||||
name: 'markdown',
|
||||
})
|
||||
export class MarkdownPipe implements PipeTransform {
|
||||
transform(value: any): any {
|
||||
transform(value: string): string {
|
||||
if (value && value.length > 0) {
|
||||
// convert markdown to html
|
||||
const html = marked(value)
|
||||
// sanitize html
|
||||
const sanitized = DOMPurify.sanitize(html)
|
||||
return sanitized
|
||||
// parse html to find all links
|
||||
let parser = new DOMParser()
|
||||
const doc = parser.parseFromString(sanitized, 'text/html')
|
||||
const links = Array.from(doc.getElementsByTagName('a'))
|
||||
// add target="_blank" to every link
|
||||
links.forEach(link => {
|
||||
link.setAttribute('target', '_blank')
|
||||
})
|
||||
// return new html string
|
||||
return doc.documentElement.innerHTML
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -40,12 +40,14 @@ export * from './services/error-toast.service'
|
||||
export * from './services/http.service'
|
||||
|
||||
export * from './types/api'
|
||||
export * from './types/rpc-error-details'
|
||||
export * from './types/http.types'
|
||||
export * from './types/rpc.types'
|
||||
export * from './types/url'
|
||||
export * from './types/workspace-config'
|
||||
|
||||
export * from './util/copy-to-clipboard'
|
||||
export * from './util/get-pkg-id'
|
||||
export * from './util/misc.util'
|
||||
export * from './util/rpc.util'
|
||||
export * from './util/to-local-iso-string'
|
||||
export * from './util/unused'
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||
import { HttpError, RpcError, WorkspaceConfig } from '@start9labs/shared'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { HttpError } from '../classes/http-error'
|
||||
import {
|
||||
HttpAngularOptions,
|
||||
HttpOptions,
|
||||
LocalHttpResponse,
|
||||
Method,
|
||||
} from '../types/http.types'
|
||||
import { RPCResponse, RPCOptions } from '../types/rpc.types'
|
||||
import { WorkspaceConfig } from '../types/workspace-config'
|
||||
import {
|
||||
firstValueFrom,
|
||||
from,
|
||||
@@ -32,20 +40,21 @@ export class HttpService {
|
||||
this.fullUrl = `${protocol}//${hostname}:${port}`
|
||||
}
|
||||
|
||||
async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const { method, params, timeout } = opts
|
||||
async rpcRequest<T>(
|
||||
opts: RPCOptions,
|
||||
): Promise<LocalHttpResponse<RPCResponse<T>>> {
|
||||
const { method, headers, params, timeout } = opts
|
||||
|
||||
const res = await this.httpRequest<RPCResponse<T>>({
|
||||
return this.httpRequest<RPCResponse<T>>({
|
||||
method: Method.POST,
|
||||
url: this.relativeUrl,
|
||||
headers,
|
||||
body: { method, params },
|
||||
timeout,
|
||||
})
|
||||
if (isRpcError(res)) throw new RpcError(res.error)
|
||||
return res.result
|
||||
}
|
||||
|
||||
async httpRequest<T>(opts: HttpOptions): Promise<T> {
|
||||
async httpRequest<T>(opts: HttpOptions): Promise<LocalHttpResponse<T>> {
|
||||
let { method, url, headers, body, responseType, timeout } = opts
|
||||
|
||||
url = opts.url.startsWith('/') ? this.fullUrl + url : url
|
||||
@@ -67,113 +76,21 @@ export class HttpService {
|
||||
responseType: responseType || 'json',
|
||||
}
|
||||
|
||||
let req: Observable<{ body: T }>
|
||||
let req: Observable<LocalHttpResponse<T>>
|
||||
if (method === Method.GET) {
|
||||
req = this.http.get(url, options as any) as any
|
||||
} else {
|
||||
req = this.http.post(url, body, options as any) as any
|
||||
}
|
||||
|
||||
return firstValueFrom(timeout ? withTimeout(req, timeout) : req)
|
||||
.then(res => res.body)
|
||||
.catch(e => {
|
||||
return firstValueFrom(timeout ? withTimeout(req, timeout) : req).catch(
|
||||
e => {
|
||||
throw new HttpError(e)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ** RPC types **
|
||||
|
||||
interface RPCBase {
|
||||
jsonrpc: '2.0'
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface RPCRequest<T> extends RPCBase {
|
||||
method: string
|
||||
params?: T
|
||||
}
|
||||
|
||||
export interface RPCSuccess<T> extends RPCBase {
|
||||
result: T
|
||||
}
|
||||
|
||||
export interface RPCError extends RPCBase {
|
||||
error: {
|
||||
code: number
|
||||
message: string
|
||||
data?:
|
||||
| {
|
||||
details: string
|
||||
}
|
||||
| string
|
||||
}
|
||||
}
|
||||
|
||||
export type RPCResponse<T> = RPCSuccess<T> | RPCError
|
||||
|
||||
export interface RPCOptions {
|
||||
method: string
|
||||
params: {
|
||||
[param: string]:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| object
|
||||
| string[]
|
||||
| number[]
|
||||
| null
|
||||
}
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export function isRpcError<Error, Result>(
|
||||
arg: { error: Error } | { result: Result },
|
||||
): arg is { error: Error } {
|
||||
return (arg as any).error !== undefined
|
||||
}
|
||||
|
||||
// ** HTTP types **
|
||||
|
||||
export enum Method {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
}
|
||||
|
||||
export interface HttpOptions {
|
||||
method: Method
|
||||
url: string
|
||||
headers?:
|
||||
| HttpHeaders
|
||||
| {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params?:
|
||||
| HttpParams
|
||||
| {
|
||||
[param: string]: string | string[]
|
||||
}
|
||||
responseType?: 'json' | 'text' | 'arrayBuffer'
|
||||
body?: any
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface HttpAngularOptions {
|
||||
observe: 'response'
|
||||
withCredentials: true
|
||||
headers?:
|
||||
| HttpHeaders
|
||||
| {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params?:
|
||||
| HttpParams
|
||||
| {
|
||||
[param: string]: string | string[]
|
||||
}
|
||||
responseType?: 'json' | 'text' | 'arrayBuffer'
|
||||
}
|
||||
|
||||
function hasParams(
|
||||
params?: HttpOptions['params'],
|
||||
): params is Record<string, string | string[]> {
|
||||
@@ -191,9 +108,3 @@ function withTimeout<U>(req: Observable<U>, timeout: number): Observable<U> {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export interface RequestError {
|
||||
code: number
|
||||
message: string
|
||||
details: string
|
||||
}
|
||||
|
||||
44
frontend/projects/shared/src/types/http.types.ts
Normal file
44
frontend/projects/shared/src/types/http.types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http'
|
||||
|
||||
export enum Method {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
}
|
||||
|
||||
export interface HttpOptions {
|
||||
method: Method
|
||||
url: string
|
||||
headers?: {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params?: {
|
||||
[param: string]: string | string[]
|
||||
}
|
||||
responseType?: 'json' | 'text' | 'arrayBuffer'
|
||||
body?: any
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export interface HttpAngularOptions {
|
||||
observe: 'response'
|
||||
withCredentials: true
|
||||
headers?:
|
||||
| HttpHeaders
|
||||
| {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params?: {
|
||||
[param: string]: string | string[]
|
||||
}
|
||||
responseType?: 'json' | 'text' | 'arrayBuffer'
|
||||
}
|
||||
|
||||
export interface LocalHttpResponse<T> extends HttpResponse<T> {
|
||||
body: T
|
||||
}
|
||||
|
||||
export interface RequestError {
|
||||
code: number
|
||||
message: string
|
||||
details: string
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface RpcErrorDetails<T> {
|
||||
code: number
|
||||
message: string
|
||||
data?:
|
||||
| {
|
||||
details: string
|
||||
revision?: T | null
|
||||
}
|
||||
| string
|
||||
}
|
||||
55
frontend/projects/shared/src/types/rpc.types.ts
Normal file
55
frontend/projects/shared/src/types/rpc.types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// ** RPC types **
|
||||
|
||||
interface RPCBase {
|
||||
jsonrpc: '2.0'
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface RPCRequest<T> extends RPCBase {
|
||||
method: string
|
||||
params?: T
|
||||
}
|
||||
|
||||
export interface RPCSuccessRes<T> extends RPCBase {
|
||||
result: T
|
||||
}
|
||||
|
||||
export interface RPCErrorRes extends RPCBase {
|
||||
error: RPCErrorDetails
|
||||
}
|
||||
|
||||
export interface RPCErrorDetails {
|
||||
code: number
|
||||
message: string
|
||||
data?:
|
||||
| {
|
||||
details: string
|
||||
}
|
||||
| string
|
||||
}
|
||||
|
||||
export type RPCResponse<T> = RPCSuccessRes<T> | RPCErrorRes
|
||||
|
||||
export interface RPCOptions {
|
||||
method: string
|
||||
headers?: {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params: {
|
||||
[param: string]:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| object
|
||||
| string[]
|
||||
| number[]
|
||||
| null
|
||||
}
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export function isRpcError<Error, Result>(
|
||||
arg: { error: Error } | { result: Result },
|
||||
): arg is { error: Error } {
|
||||
return (arg as any).error !== undefined
|
||||
}
|
||||
11
frontend/projects/shared/src/util/rpc.util.ts
Normal file
11
frontend/projects/shared/src/util/rpc.util.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { RPCErrorDetails } from '../types/rpc.types'
|
||||
|
||||
export function getRpcErrorMessage(error: RPCErrorDetails): string {
|
||||
if (typeof error.data === 'string') {
|
||||
return `${error.message}\n\n${error.data}`
|
||||
}
|
||||
|
||||
return error.data?.details
|
||||
? `${error.message}\n\n${error.data.details}`
|
||||
: error.message
|
||||
}
|
||||
@@ -24,3 +24,6 @@
|
||||
@import "~@ionic/angular/css/text-alignment.css";
|
||||
@import "~@ionic/angular/css/text-transformation.css";
|
||||
@import "~@ionic/angular/css/flex-utils.css";
|
||||
|
||||
/* Import swiper styles for slides */
|
||||
@import '~swiper/scss';
|
||||
|
||||
@@ -18,6 +18,12 @@ ion-alert {
|
||||
}
|
||||
}
|
||||
|
||||
.swiper {
|
||||
.swiper-slide {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
ion-modal::part(content) {
|
||||
position: absolute;
|
||||
height: 90% !important;
|
||||
|
||||
@@ -34,7 +34,7 @@ import { ConnectionBarComponentModule } from './components/connection-bar/connec
|
||||
storeName: '_embassykv',
|
||||
dbKey: '_embassykey',
|
||||
name: '_embassystorage',
|
||||
driverOrder: [Drivers.LocalStorage, Drivers.IndexedDB],
|
||||
driverOrder: [Drivers.LocalStorage],
|
||||
}),
|
||||
MenuModule,
|
||||
PreloaderModule,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Bootstrapper, DBCache } from 'patch-db-client'
|
||||
import { APP_INITIALIZER, ErrorHandler, Provider } from '@angular/core'
|
||||
import { APP_INITIALIZER, Provider } from '@angular/core'
|
||||
import { UntypedFormBuilder } from '@angular/forms'
|
||||
import { Router, RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicRouteStrategy, IonNav } from '@ionic/angular'
|
||||
@@ -8,10 +7,8 @@ import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||
import { BOOTSTRAPPER, PATCH_CACHE } from './services/patch-db/patch-db.factory'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { LocalStorageService } from './services/local-storage.service'
|
||||
import { DataModel } from './services/patch-db/data-model'
|
||||
import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe'
|
||||
|
||||
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
@@ -30,14 +27,7 @@ export const APP_PROVIDERS: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
deps: [
|
||||
Storage,
|
||||
AuthService,
|
||||
LocalStorageService,
|
||||
Router,
|
||||
BOOTSTRAPPER,
|
||||
PATCH_CACHE,
|
||||
],
|
||||
deps: [Storage, AuthService, LocalStorageService, Router],
|
||||
useFactory: appInitializer,
|
||||
multi: true,
|
||||
},
|
||||
@@ -48,19 +38,12 @@ export function appInitializer(
|
||||
auth: AuthService,
|
||||
localStorage: LocalStorageService,
|
||||
router: Router,
|
||||
bootstrapper: Bootstrapper<DataModel>,
|
||||
cache: DBCache<DataModel>,
|
||||
): () => Promise<void> {
|
||||
return async () => {
|
||||
await storage.create()
|
||||
await auth.init()
|
||||
await localStorage.init()
|
||||
|
||||
const localCache = await bootstrapper.init()
|
||||
|
||||
cache.sequence = localCache.sequence
|
||||
cache.data = localCache.data
|
||||
|
||||
router.initialNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { heightCollapse } from '../../util/animations'
|
||||
import { PatchDbService } from '../../services/patch-db/patch-db.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { ServerInfo } from '../../services/patch-db/data-model'
|
||||
import { DataModel, ServerInfo } from '../../services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'footer[appFooter]',
|
||||
@@ -24,7 +24,7 @@ export class FooterComponent {
|
||||
},
|
||||
}
|
||||
|
||||
constructor(private readonly patch: PatchDbService) {}
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
|
||||
getProgress({
|
||||
downloaded,
|
||||
|
||||
@@ -57,6 +57,6 @@
|
||||
src="assets/img/icons/snek.png"
|
||||
[appSnekHighScore]="snekScore$ | async"
|
||||
/>
|
||||
<ion-footer class="bottom">
|
||||
<ion-footer *ngIf="sidebarOpen$ | async" class="bottom">
|
||||
<connection-bar></connection-bar>
|
||||
</ion-footer>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { LocalStorageService } from '../../services/local-storage.service'
|
||||
import { EOSService } from '../../services/eos.service'
|
||||
import { PatchDbService } from '../../services/patch-db/patch-db.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
@@ -57,11 +59,14 @@ export class MenuComponent {
|
||||
.getUpdates()
|
||||
.pipe(map(pkgs => pkgs.length))
|
||||
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly localStorageService: LocalStorageService,
|
||||
private readonly eosService: EOSService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
<!-- 3rd party components -->
|
||||
<qr-code value="hello"></qr-code>
|
||||
<swiper>
|
||||
<ng-template swiperSlide>Slide 1</ng-template>
|
||||
</swiper>
|
||||
|
||||
<!-- Ionic components -->
|
||||
<ion-action-sheet></ion-action-sheet>
|
||||
|
||||
@@ -2,11 +2,10 @@ import { CommonModule } from '@angular/common'
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
import { SwiperModule } from 'swiper/angular'
|
||||
import { PreloaderComponent } from './preloader.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, QrCodeModule, SwiperModule],
|
||||
imports: [CommonModule, IonicModule, QrCodeModule],
|
||||
declarations: [PreloaderComponent],
|
||||
exports: [PreloaderComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Directive, HostListener, Input } from '@angular/core'
|
||||
import { LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
import { SnakePage } from '../../modals/snake/snake.page'
|
||||
import { PatchDbService } from '../../services/patch-db/patch-db.service'
|
||||
import { ApiService } from '../../services/api/embassy-api.service'
|
||||
|
||||
@Directive({
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<h1>
|
||||
<ion-text color="warning">Warning</ion-text>
|
||||
</h1>
|
||||
<div class="ion-text-left" [innerHTML]="params.message || '' | markdown"></div>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { AlertComponent } from './alert.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { MarkdownPipeModule } from '@start9labs/shared'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AlertComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
MarkdownPipeModule,
|
||||
],
|
||||
exports: [AlertComponent],
|
||||
})
|
||||
export class AlertComponentModule {}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { BaseSlide } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'alert',
|
||||
templateUrl: './alert.component.html',
|
||||
styleUrls: ['../app-wizard.component.scss'],
|
||||
})
|
||||
export class AlertComponent implements BaseSlide {
|
||||
@Input()
|
||||
params!: { message: string }
|
||||
|
||||
async load() {}
|
||||
|
||||
loading = false
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<div style="padding: 10px 0">
|
||||
<ion-title style="font-size: 32px">{{ params.title }}</ion-title>
|
||||
<div class="underline"></div>
|
||||
<ion-title>
|
||||
<i
|
||||
>{{ params.action | titlecase
|
||||
}}<span *ngIf="params.version"
|
||||
>: {{ params.version | displayEmver }}</span
|
||||
></i
|
||||
>
|
||||
</ion-title>
|
||||
</div>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<div style="padding: 36px; height: 100%">
|
||||
<swiper
|
||||
*ngIf="!error; else hasError"
|
||||
(swiper)="setSwiperInstance($event)"
|
||||
(slideNextTransitionStart)="loadSlide()"
|
||||
>
|
||||
<ng-template swiperSlide *ngFor="let slide of params.slides">
|
||||
<alert
|
||||
#components
|
||||
*ngIf="slide.selector === 'alert'"
|
||||
[params]="slide.params"
|
||||
></alert>
|
||||
<dependents
|
||||
#components
|
||||
*ngIf="slide.selector === 'dependents'"
|
||||
[params]="slide.params"
|
||||
(onSuccess)="next()"
|
||||
(onError)="setError($event)"
|
||||
></dependents>
|
||||
<complete
|
||||
#components
|
||||
*ngIf="slide.selector === 'complete'"
|
||||
[params]="slide.params"
|
||||
(onSuccess)="dismiss('success')"
|
||||
(onError)="setError($event)"
|
||||
></complete>
|
||||
</ng-template>
|
||||
</swiper>
|
||||
|
||||
<ng-template #hasError>
|
||||
<p>
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ng-container *ngIf="!initializing && swiper">
|
||||
<ion-buttons slot="end" class="ion-padding-end">
|
||||
<ion-button
|
||||
*ngIf="error; else noError"
|
||||
fill="solid"
|
||||
color="dark"
|
||||
(click)="dismiss()"
|
||||
class="enter-click btn-128"
|
||||
>
|
||||
Dismiss
|
||||
</ion-button>
|
||||
<ng-template #noError>
|
||||
<ion-button
|
||||
*ngIf="!currentSlide.loading && !swiper.isEnd"
|
||||
fill="solid"
|
||||
color="primary"
|
||||
(click)="next()"
|
||||
class="enter-click btn-128"
|
||||
[class.no-click]="currentSlide.loading"
|
||||
>
|
||||
{{
|
||||
currentIndex < swiper.slides.length - 2
|
||||
? 'Continue'
|
||||
: params.submitBtn
|
||||
}}
|
||||
</ion-button>
|
||||
</ng-template>
|
||||
</ion-buttons>
|
||||
</ng-container>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
@@ -1,26 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { AppWizardComponent } from './app-wizard.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { EmverPipesModule } from '@start9labs/shared'
|
||||
import { DependentsComponentModule } from './dependents/dependents.component.module'
|
||||
import { CompleteComponentModule } from './complete/complete.component.module'
|
||||
import { AlertComponentModule } from './alert/alert.component.module'
|
||||
import { SwiperModule } from 'swiper/angular'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppWizardComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
EmverPipesModule,
|
||||
DependentsComponentModule,
|
||||
CompleteComponentModule,
|
||||
AlertComponentModule,
|
||||
SwiperModule,
|
||||
],
|
||||
exports: [AppWizardComponent],
|
||||
})
|
||||
export class AppWizardComponentModule {}
|
||||
@@ -1,6 +0,0 @@
|
||||
.underline {
|
||||
margin: 6px 0 8px 16px;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #404040;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { IonContent, ModalController } from '@ionic/angular'
|
||||
import { CompleteComponent } from './complete/complete.component'
|
||||
import { DependentsComponent } from './dependents/dependents.component'
|
||||
import { AlertComponent } from './alert/alert.component'
|
||||
import { WizardAction } from './wizard-types'
|
||||
import SwiperCore, { Swiper } from 'swiper'
|
||||
import { IonicSlides } from '@ionic/angular'
|
||||
import { BaseSlide } from './wizard-types'
|
||||
|
||||
SwiperCore.use([IonicSlides])
|
||||
|
||||
@Component({
|
||||
selector: 'app-wizard',
|
||||
templateUrl: './app-wizard.component.html',
|
||||
styleUrls: ['./app-wizard.component.scss'],
|
||||
})
|
||||
export class AppWizardComponent {
|
||||
@Input()
|
||||
params!: {
|
||||
action: WizardAction
|
||||
title: string
|
||||
slides: SlideDefinition[]
|
||||
submitBtn: string
|
||||
version?: string
|
||||
}
|
||||
|
||||
// content container so we can scroll to top between slide transitions
|
||||
@ViewChild(IonContent)
|
||||
content?: IonContent
|
||||
|
||||
swiper?: Swiper
|
||||
|
||||
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view
|
||||
@ViewChildren('components')
|
||||
slideComponentsQL?: QueryList<BaseSlide>
|
||||
|
||||
get slideComponents(): BaseSlide[] {
|
||||
return this.slideComponentsQL?.toArray() || []
|
||||
}
|
||||
|
||||
get currentSlide(): BaseSlide {
|
||||
return this.slideComponents[this.currentIndex]
|
||||
}
|
||||
|
||||
get currentIndex(): number {
|
||||
return this.swiper?.activeIndex || NaN
|
||||
}
|
||||
|
||||
initializing = true
|
||||
error = ''
|
||||
|
||||
constructor(private readonly modalController: ModalController) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
this.initializing = false
|
||||
if (this.swiper) this.swiper.allowTouchMove = false
|
||||
this.loadSlide()
|
||||
}
|
||||
|
||||
setSwiperInstance(swiper: any) {
|
||||
this.swiper = swiper
|
||||
}
|
||||
|
||||
dismiss(role = 'cancelled') {
|
||||
this.modalController.dismiss(null, role)
|
||||
}
|
||||
|
||||
async next() {
|
||||
await this.content?.scrollToTop()
|
||||
this.swiper?.slideNext(500)
|
||||
}
|
||||
|
||||
setError(e: any) {
|
||||
this.error = e
|
||||
}
|
||||
|
||||
async loadSlide() {
|
||||
this.currentSlide.load()
|
||||
}
|
||||
}
|
||||
|
||||
export type SlideDefinition =
|
||||
| { selector: 'alert'; params: AlertComponent['params'] }
|
||||
| { selector: 'dependents'; params: DependentsComponent['params'] }
|
||||
| { selector: 'complete'; params: CompleteComponent['params'] }
|
||||
|
||||
export async function wizardModal(
|
||||
modalController: ModalController,
|
||||
params: AppWizardComponent['params'],
|
||||
): Promise<boolean> {
|
||||
const modal = await modalController.create({
|
||||
backdropDismiss: false,
|
||||
cssClass: 'wizard-modal',
|
||||
component: AppWizardComponent,
|
||||
componentProps: { params },
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
return modal.onDidDismiss().then(({ role }) => role === 'success')
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<div style="padding: 32px">
|
||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user