mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
Compare commits
94 Commits
st/port-la
...
feat/prefe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c672c024 | ||
|
|
2fd87298bf | ||
|
|
ee7f77b5db | ||
|
|
cdf30196ca | ||
|
|
e999d89bbc | ||
|
|
16a2fe4e08 | ||
|
|
6778f37307 | ||
|
|
b51bfb8d59 | ||
|
|
0e15a6e7ed | ||
|
|
f004c46977 | ||
|
|
011a3f9d9f | ||
|
|
b1c533d670 | ||
|
|
d0ac073651 | ||
|
|
6c86146e94 | ||
|
|
e74f8db887 | ||
|
|
d422cd3c66 | ||
|
|
7f66c62848 | ||
|
|
7e8be5852d | ||
|
|
72d573dbd1 | ||
|
|
827458562b | ||
|
|
803dd38d96 | ||
|
|
8da9d76cb4 | ||
|
|
b466e71b3b | ||
|
|
3743a0d2e4 | ||
|
|
33a51bc663 | ||
|
|
d69e5b9f1a | ||
|
|
d4e019c87b | ||
|
|
3974c09369 | ||
|
|
86ecc4cc99 | ||
|
|
d1162272f0 | ||
|
|
5294e8f444 | ||
|
|
b7da7cd59f | ||
|
|
bee8a0f9d8 | ||
|
|
0724989792 | ||
|
|
31352a72c3 | ||
|
|
c7a4f0f9cb | ||
|
|
7879668c40 | ||
|
|
6a01b5eab1 | ||
|
|
80cb2d9ba5 | ||
|
|
8c1a452742 | ||
|
|
135afd0251 | ||
|
|
35f3274f29 | ||
|
|
9af5b87c92 | ||
|
|
66b5bc1897 | ||
|
|
7909941b70 | ||
|
|
4527046f2e | ||
|
|
5a292e6e2a | ||
|
|
84149be3c1 | ||
|
|
d562466fc4 | ||
|
|
9c3053f103 | ||
|
|
dce975410f | ||
|
|
783ce4b3b6 | ||
|
|
675a03bdc5 | ||
|
|
485fced691 | ||
|
|
a22707c1cb | ||
|
|
74e10ec473 | ||
|
|
e25e0f0c12 | ||
|
|
4cae00cb33 | ||
|
|
313b2df540 | ||
|
|
5fbc73755d | ||
|
|
bc4478b0b9 | ||
|
|
68141112b7 | ||
|
|
ccafb599a6 | ||
|
|
52272feb3e | ||
|
|
1abad93646 | ||
|
|
c9468dda02 | ||
|
|
6a1b1627c5 | ||
|
|
cfbace1d91 | ||
|
|
d97ab59bab | ||
|
|
3518eccc87 | ||
|
|
2f19188dae | ||
|
|
3a63f3b840 | ||
|
|
098d9275f4 | ||
|
|
d5c74bc22e | ||
|
|
49d4da03ca | ||
|
|
3765465618 | ||
|
|
61f820d09e | ||
|
|
db7f3341ac | ||
|
|
4decf9335c | ||
|
|
339e5f799a | ||
|
|
89d3e0cf35 | ||
|
|
638ed27599 | ||
|
|
da75b8498e | ||
|
|
8ef4ecf5ac | ||
|
|
2a54625f43 | ||
|
|
4e638fb58e | ||
|
|
73274ef6e0 | ||
|
|
e1915bf497 | ||
|
|
8204074bdf | ||
|
|
2ee403e7de | ||
|
|
1974dfd66f | ||
|
|
2e03a95e47 | ||
|
|
8f809dab21 | ||
|
|
c0b2cbe1c8 |
@@ -62,27 +62,12 @@ fi
|
||||
chroot /media/startos/next bash -e << "EOF"
|
||||
|
||||
if [ -f /boot/grub/grub.cfg ]; then
|
||||
grub-install /dev/$(eval $(lsblk -o MOUNTPOINT,PKNAME -P | grep 'MOUNTPOINT="/media/startos/root"') && echo $PKNAME)
|
||||
grub-install --no-nvram /dev/$(eval $(lsblk -o MOUNTPOINT,PKNAME -P | grep 'MOUNTPOINT="/media/startos/root"') && echo $PKNAME)
|
||||
update-grub
|
||||
fi
|
||||
|
||||
EOF
|
||||
|
||||
# Promote the USB installer boot entry back to first in EFI boot order.
|
||||
# The entry number was saved during initial OS install.
|
||||
if [ -d /sys/firmware/efi ] && [ -f /media/startos/config/efi-installer-entry ]; then
|
||||
USB_ENTRY=$(cat /media/startos/config/efi-installer-entry)
|
||||
if [ -n "$USB_ENTRY" ]; then
|
||||
CURRENT_ORDER=$(efibootmgr | grep BootOrder | sed 's/BootOrder: //')
|
||||
OTHER_ENTRIES=$(echo "$CURRENT_ORDER" | tr ',' '\n' | grep -v "$USB_ENTRY" | tr '\n' ',' | sed 's/,$//')
|
||||
if [ -n "$OTHER_ENTRIES" ]; then
|
||||
efibootmgr -o "$USB_ENTRY,$OTHER_ENTRIES"
|
||||
else
|
||||
efibootmgr -o "$USB_ENTRY"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
sync
|
||||
|
||||
umount -Rl /media/startos/next
|
||||
|
||||
2
container-runtime/package-lock.json
generated
2
container-runtime/package-lock.json
generated
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"../sdk/dist": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.58",
|
||||
"version": "0.4.0-beta.55",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -25,4 +25,3 @@ cd sdk && make baseDist dist # Rebuild SDK after ts-bindings
|
||||
- When adding i18n keys, add all 5 locales in `core/locales/i18n.yaml` (see [i18n-patterns.md](i18n-patterns.md))
|
||||
- When using DB watches, follow the `TypedDbWatch<T>` patterns in [patchdb.md](patchdb.md)
|
||||
- **Always use `.invoke(ErrorKind::...)` instead of `.status()` when running CLI commands** via `tokio::process::Command`. The `Invoke` trait (from `crate::util::Invoke`) captures stdout/stderr and checks exit codes properly. Using `.status()` leaks stderr directly to system logs, creating noise. For check-then-act patterns (e.g. `iptables -C`), use `.invoke(...).await.is_ok()` / `.is_err()` instead of `.status().await.map_or(false, |s| s.success())`.
|
||||
- Always use file utils in util::io instead of tokio::fs when available
|
||||
|
||||
@@ -174,11 +174,11 @@ async fn set_name(
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CheckPortParams {
|
||||
struct CheckPortParams {
|
||||
#[arg(help = "help.arg.port")]
|
||||
pub port: u16,
|
||||
port: u16,
|
||||
#[arg(help = "help.arg.gateway-id")]
|
||||
pub gateway: GatewayId,
|
||||
gateway: GatewayId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||
@@ -200,7 +200,7 @@ pub struct IfconfigPortRes {
|
||||
pub reachable: bool,
|
||||
}
|
||||
|
||||
pub async fn check_port(
|
||||
async fn check_port(
|
||||
ctx: RpcContext,
|
||||
CheckPortParams { port, gateway }: CheckPortParams,
|
||||
) -> Result<CheckPortRes, Error> {
|
||||
@@ -276,12 +276,12 @@ pub async fn check_port(
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CheckDnsParams {
|
||||
struct CheckDnsParams {
|
||||
#[arg(help = "help.arg.gateway-id")]
|
||||
pub gateway: GatewayId,
|
||||
gateway: GatewayId,
|
||||
}
|
||||
|
||||
pub async fn check_dns(
|
||||
async fn check_dns(
|
||||
ctx: RpcContext,
|
||||
CheckDnsParams { gateway }: CheckDnsParams,
|
||||
) -> Result<bool, Error> {
|
||||
@@ -1238,7 +1238,8 @@ async fn poll_ip_info(
|
||||
device_type,
|
||||
Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback)
|
||||
) {
|
||||
let res = match get_wan_ipv4(iface.as_str(), &ifconfig_url).await {
|
||||
*prev_attempt = Some(Instant::now());
|
||||
match get_wan_ipv4(iface.as_str(), &ifconfig_url).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
@@ -1252,9 +1253,7 @@ async fn poll_ip_info(
|
||||
tracing::debug!("{e:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
*prev_attempt = Some(Instant::now());
|
||||
res
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::hostname::ServerHostname;
|
||||
use crate::net::acme::AcmeProvider;
|
||||
use crate::net::gateway::{CheckDnsParams, CheckPortParams, CheckPortRes, check_dns, check_port};
|
||||
use crate::net::host::{HostApiKind, all_hosts};
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
@@ -161,7 +160,6 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddPublicDomainParams {
|
||||
#[arg(help = "help.arg.fqdn")]
|
||||
@@ -170,17 +168,6 @@ pub struct AddPublicDomainParams {
|
||||
pub acme: Option<AcmeProvider>,
|
||||
#[arg(help = "help.arg.gateway-id")]
|
||||
pub gateway: GatewayId,
|
||||
#[arg(help = "help.arg.internal-port")]
|
||||
pub internal_port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddPublicDomainRes {
|
||||
#[ts(type = "string | null")]
|
||||
pub dns: Option<Ipv4Addr>,
|
||||
pub port: CheckPortRes,
|
||||
}
|
||||
|
||||
pub async fn add_public_domain<Kind: HostApiKind>(
|
||||
@@ -189,12 +176,10 @@ pub async fn add_public_domain<Kind: HostApiKind>(
|
||||
fqdn,
|
||||
acme,
|
||||
gateway,
|
||||
internal_port,
|
||||
}: AddPublicDomainParams,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<AddPublicDomainRes, Error> {
|
||||
let ext_port = ctx
|
||||
.db
|
||||
) -> Result<Option<Ipv4Addr>, Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
if let Some(acme) = &acme {
|
||||
if !db
|
||||
@@ -210,92 +195,21 @@ pub async fn add_public_domain<Kind: HostApiKind>(
|
||||
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_public_domains_mut()
|
||||
.insert(
|
||||
&fqdn,
|
||||
&PublicDomainConfig {
|
||||
acme,
|
||||
gateway: gateway.clone(),
|
||||
},
|
||||
)?;
|
||||
.insert(&fqdn, &PublicDomainConfig { acme, gateway })?;
|
||||
handle_duplicates(db)?;
|
||||
let hostname = ServerHostname::load(db.as_public().as_server_info())?;
|
||||
let gateways = db
|
||||
.as_public()
|
||||
.as_server_info()
|
||||
.as_network()
|
||||
.as_gateways()
|
||||
.de()?;
|
||||
let available_ports = db.as_private().as_available_ports().de()?;
|
||||
let host = Kind::host_for(&inheritance, db)?;
|
||||
host.update_addresses(&hostname, &gateways, &available_ports)?;
|
||||
|
||||
// Find the external port for the target binding
|
||||
let bindings = host.as_bindings().de()?;
|
||||
let target_bind = bindings
|
||||
.get(&internal_port)
|
||||
.ok_or_else(|| Error::new(eyre!("binding not found for internal port {internal_port}"), ErrorKind::NotFound))?;
|
||||
let ext_port = target_bind
|
||||
.addresses
|
||||
.available
|
||||
.iter()
|
||||
.find(|a| a.public && a.hostname == fqdn)
|
||||
.and_then(|a| a.port)
|
||||
.ok_or_else(|| Error::new(eyre!("no public address found for {fqdn} on port {internal_port}"), ErrorKind::NotFound))?;
|
||||
|
||||
// Disable the domain on all other bindings
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
for (&port, bind) in b.iter_mut() {
|
||||
if port == internal_port {
|
||||
continue;
|
||||
}
|
||||
let has_addr = bind
|
||||
.addresses
|
||||
.available
|
||||
.iter()
|
||||
.any(|a| a.public && a.hostname == fqdn);
|
||||
if has_addr {
|
||||
let other_ext = bind
|
||||
.addresses
|
||||
.available
|
||||
.iter()
|
||||
.find(|a| a.public && a.hostname == fqdn)
|
||||
.and_then(|a| a.port)
|
||||
.unwrap_or(ext_port);
|
||||
bind.addresses.disabled.insert((fqdn.clone(), other_ext));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(ext_port)
|
||||
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
|
||||
let ports = db.as_private().as_available_ports().de()?;
|
||||
Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let fqdn2 = fqdn.clone();
|
||||
|
||||
let (dns_result, port_result) = tokio::join!(
|
||||
async {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
crate::net::dns::query_dns(ctx2, crate::net::dns::QueryDnsParams { fqdn: fqdn2 })
|
||||
})
|
||||
.await
|
||||
.with_kind(ErrorKind::Unknown)?
|
||||
},
|
||||
check_port(
|
||||
ctx.clone(),
|
||||
CheckPortParams {
|
||||
port: ext_port,
|
||||
gateway: gateway.clone(),
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
Ok(AddPublicDomainRes {
|
||||
dns: dns_result?,
|
||||
port: port_result?,
|
||||
tokio::task::spawn_blocking(|| {
|
||||
crate::net::dns::query_dns(ctx, crate::net::dns::QueryDnsParams { fqdn })
|
||||
})
|
||||
.await
|
||||
.with_kind(ErrorKind::Unknown)?
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
@@ -343,13 +257,13 @@ pub async fn add_private_domain<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
AddPrivateDomainParams { fqdn, gateway }: AddPrivateDomainParams,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<bool, Error> {
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_private_domains_mut()
|
||||
.upsert(&fqdn, || Ok(BTreeSet::new()))?
|
||||
.mutate(|d| Ok(d.insert(gateway.clone())))?;
|
||||
.mutate(|d| Ok(d.insert(gateway)))?;
|
||||
handle_duplicates(db)?;
|
||||
let hostname = ServerHostname::load(db.as_public().as_server_info())?;
|
||||
let gateways = db
|
||||
@@ -364,7 +278,7 @@ pub async fn add_private_domain<Kind: HostApiKind>(
|
||||
.await
|
||||
.result?;
|
||||
|
||||
check_dns(ctx, CheckDnsParams { gateway }).await
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_private_domain<Kind: HostApiKind>(
|
||||
|
||||
@@ -27,63 +27,6 @@ use crate::util::serde::IoFormat;
|
||||
mod gpt;
|
||||
mod mbr;
|
||||
|
||||
/// Get the EFI BootCurrent entry number (the entry firmware used to boot).
|
||||
/// Returns None on non-EFI systems or if BootCurrent is not set.
|
||||
async fn get_efi_boot_current() -> Result<Option<String>, Error> {
|
||||
let efi_output = String::from_utf8(
|
||||
Command::new("efibootmgr")
|
||||
.invoke(ErrorKind::Grub)
|
||||
.await?,
|
||||
)
|
||||
.map_err(|e| Error::new(eyre!("efibootmgr output not valid UTF-8: {e}"), ErrorKind::Grub))?;
|
||||
|
||||
Ok(efi_output
|
||||
.lines()
|
||||
.find(|line| line.starts_with("BootCurrent:"))
|
||||
.and_then(|line| line.strip_prefix("BootCurrent:"))
|
||||
.map(|s| s.trim().to_string()))
|
||||
}
|
||||
|
||||
/// Promote a specific boot entry to first in the EFI boot order.
|
||||
async fn promote_efi_entry(entry: &str) -> Result<(), Error> {
|
||||
let efi_output = String::from_utf8(
|
||||
Command::new("efibootmgr")
|
||||
.invoke(ErrorKind::Grub)
|
||||
.await?,
|
||||
)
|
||||
.map_err(|e| Error::new(eyre!("efibootmgr output not valid UTF-8: {e}"), ErrorKind::Grub))?;
|
||||
|
||||
let current_order = efi_output
|
||||
.lines()
|
||||
.find(|line| line.starts_with("BootOrder:"))
|
||||
.and_then(|line| line.strip_prefix("BootOrder:"))
|
||||
.map(|s| s.trim())
|
||||
.unwrap_or("");
|
||||
|
||||
if current_order.is_empty() || current_order.starts_with(entry) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let other_entries: Vec<&str> = current_order
|
||||
.split(',')
|
||||
.filter(|e| e.trim() != entry)
|
||||
.collect();
|
||||
|
||||
let new_order = if other_entries.is_empty() {
|
||||
entry.to_string()
|
||||
} else {
|
||||
format!("{},{}", entry, other_entries.join(","))
|
||||
};
|
||||
|
||||
Command::new("efibootmgr")
|
||||
.arg("-o")
|
||||
.arg(&new_order)
|
||||
.invoke(ErrorKind::Grub)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Probe a squashfs image to determine its target architecture
|
||||
async fn probe_squashfs_arch(squashfs_path: &Path) -> Result<InternedString, Error> {
|
||||
let output = String::from_utf8(
|
||||
@@ -416,6 +359,7 @@ pub async fn install_os_to(
|
||||
"riscv64" => install.arg("--target=riscv64-efi"),
|
||||
_ => &mut install,
|
||||
};
|
||||
install.arg("--no-nvram");
|
||||
}
|
||||
install
|
||||
.arg(disk_path)
|
||||
@@ -485,21 +429,6 @@ pub async fn install_os(
|
||||
});
|
||||
|
||||
let use_efi = tokio::fs::metadata("/sys/firmware/efi").await.is_ok();
|
||||
|
||||
// Save the boot entry we booted from (the USB installer) before grub-install
|
||||
// overwrites the boot order.
|
||||
let boot_current = if use_efi {
|
||||
match get_efi_boot_current().await {
|
||||
Ok(entry) => entry,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to get EFI BootCurrent: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let InstallOsResult { part_info, rootfs } = install_os_to(
|
||||
"/run/live/medium/live/filesystem.squashfs",
|
||||
&disk.logicalname,
|
||||
@@ -511,20 +440,6 @@ pub async fn install_os(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// grub-install prepends its new entry to the EFI boot order, overriding the
|
||||
// USB-first priority. Promote the USB entry (identified by BootCurrent from
|
||||
// when we booted the installer) back to first, and persist the entry number
|
||||
// so the upgrade script can do the same.
|
||||
if let Some(ref entry) = boot_current {
|
||||
if let Err(e) = promote_efi_entry(entry).await {
|
||||
tracing::warn!("Failed to restore EFI boot order: {e}");
|
||||
}
|
||||
let efi_entry_path = rootfs.path().join("config/efi-installer-entry");
|
||||
if let Err(e) = tokio::fs::write(&efi_entry_path, entry).await {
|
||||
tracing::warn!("Failed to save EFI installer entry number: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
ctx.config
|
||||
.mutate(|c| c.os_partitions = Some(part_info.clone()));
|
||||
|
||||
|
||||
@@ -1238,13 +1238,19 @@ pub async fn test_smtp(
|
||||
.body("This is a test email sent from your StartOS Server".to_owned())?;
|
||||
|
||||
let transport = match security {
|
||||
SmtpSecurity::Starttls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&host)?,
|
||||
SmtpSecurity::Tls => AsyncSmtpTransport::<Tokio1Executor>::relay(&host)?,
|
||||
}
|
||||
.port(port)
|
||||
.tls(Tls::Wrapper(TlsParameters::new(host.clone())?))
|
||||
.credentials(creds)
|
||||
.build();
|
||||
SmtpSecurity::Starttls => AsyncSmtpTransport::<Tokio1Executor>::relay(&host)?
|
||||
.port(port)
|
||||
.credentials(creds)
|
||||
.build(),
|
||||
SmtpSecurity::Tls => {
|
||||
let tls = TlsParameters::new(host.clone())?;
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&host)?
|
||||
.port(port)
|
||||
.tls(Tls::Wrapper(tls))
|
||||
.credentials(creds)
|
||||
.build()
|
||||
}
|
||||
};
|
||||
|
||||
transport.send(message).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::db::model::public::NetworkInterfaceType;
|
||||
use crate::net::forward::add_iptables_rule;
|
||||
use crate::prelude::*;
|
||||
use crate::tunnel::context::TunnelContext;
|
||||
use crate::tunnel::db::PortForwardEntry;
|
||||
use crate::tunnel::wg::{WIREGUARD_INTERFACE_NAME, WgConfig, WgSubnetClients, WgSubnetConfig};
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
|
||||
@@ -52,22 +51,6 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
||||
.no_display()
|
||||
.with_about("about.remove-port-forward")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"update-label",
|
||||
from_fn_async(update_forward_label)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("about.update-port-forward-label")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-enabled",
|
||||
from_fn_async(set_forward_enabled)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("about.enable-or-disable-port-forward")
|
||||
.with_call_remote::<CliContext>(),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
@@ -470,17 +453,11 @@ pub async fn show_config(
|
||||
pub struct AddPortForwardParams {
|
||||
source: SocketAddrV4,
|
||||
target: SocketAddrV4,
|
||||
#[arg(long)]
|
||||
label: String,
|
||||
}
|
||||
|
||||
pub async fn add_forward(
|
||||
ctx: TunnelContext,
|
||||
AddPortForwardParams {
|
||||
source,
|
||||
target,
|
||||
label,
|
||||
}: AddPortForwardParams,
|
||||
AddPortForwardParams { source, target }: AddPortForwardParams,
|
||||
) -> Result<(), Error> {
|
||||
let prefix = ctx
|
||||
.net_iface
|
||||
@@ -505,12 +482,10 @@ pub async fn add_forward(
|
||||
m.insert(source, rc);
|
||||
});
|
||||
|
||||
let entry = PortForwardEntry { target, label, enabled: true };
|
||||
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_port_forwards_mut()
|
||||
.insert(&source, &entry)
|
||||
.insert(&source, &target)
|
||||
.and_then(|replaced| {
|
||||
if replaced.is_some() {
|
||||
Err(Error::new(
|
||||
@@ -548,92 +523,3 @@ pub async fn remove_forward(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdatePortForwardLabelParams {
|
||||
source: SocketAddrV4,
|
||||
label: String,
|
||||
}
|
||||
|
||||
pub async fn update_forward_label(
|
||||
ctx: TunnelContext,
|
||||
UpdatePortForwardLabelParams { source, label }: UpdatePortForwardLabelParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_port_forwards_mut().mutate(|pf| {
|
||||
let entry = pf.0.get_mut(&source).ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Port forward from {source} not found"),
|
||||
ErrorKind::NotFound,
|
||||
)
|
||||
})?;
|
||||
entry.label = label.clone();
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetPortForwardEnabledParams {
|
||||
source: SocketAddrV4,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
pub async fn set_forward_enabled(
|
||||
ctx: TunnelContext,
|
||||
SetPortForwardEnabledParams { source, enabled }: SetPortForwardEnabledParams,
|
||||
) -> Result<(), Error> {
|
||||
let target = ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_port_forwards_mut().mutate(|pf| {
|
||||
let entry = pf.0.get_mut(&source).ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Port forward from {source} not found"),
|
||||
ErrorKind::NotFound,
|
||||
)
|
||||
})?;
|
||||
entry.enabled = enabled;
|
||||
Ok(entry.target)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
if enabled {
|
||||
let prefix = ctx
|
||||
.net_iface
|
||||
.peek(|i| {
|
||||
i.iter()
|
||||
.find_map(|(_, i)| {
|
||||
i.ip_info.as_ref().and_then(|i| {
|
||||
i.subnets
|
||||
.iter()
|
||||
.find(|s| s.contains(&IpAddr::from(*target.ip())))
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
})
|
||||
.map(|s| s.prefix_len())
|
||||
.unwrap_or(32);
|
||||
let rc = ctx
|
||||
.forward
|
||||
.add_forward(source, target, prefix, None)
|
||||
.await?;
|
||||
ctx.active_forwards.mutate(|m| {
|
||||
m.insert(source, rc);
|
||||
});
|
||||
} else {
|
||||
if let Some(rc) = ctx.active_forwards.mutate(|m| m.remove(&source)) {
|
||||
drop(rc);
|
||||
ctx.forward.gc().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -184,11 +184,7 @@ impl TunnelContext {
|
||||
}
|
||||
|
||||
let mut active_forwards = BTreeMap::new();
|
||||
for (from, entry) in peek.as_port_forwards().de()?.0 {
|
||||
if !entry.enabled {
|
||||
continue;
|
||||
}
|
||||
let to = entry.target;
|
||||
for (from, to) in peek.as_port_forwards().de()?.0 {
|
||||
let prefix = net_iface
|
||||
.peek(|i| {
|
||||
i.iter()
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Model<TunnelDatabase> {
|
||||
}
|
||||
self.as_port_forwards_mut().mutate(|pf| {
|
||||
Ok(pf.0.retain(|k, v| {
|
||||
if keep_targets.contains(v.target.ip()) {
|
||||
if keep_targets.contains(v.ip()) {
|
||||
keep_sources.insert(*k);
|
||||
true
|
||||
} else {
|
||||
@@ -70,25 +70,11 @@ fn export_bindings_tunnel_db() {
|
||||
TunnelDatabase::export_all_to("bindings/tunnel").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PortForwardEntry {
|
||||
pub target: SocketAddrV4,
|
||||
#[serde(default)]
|
||||
pub label: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
|
||||
pub struct PortForwards(pub BTreeMap<SocketAddrV4, PortForwardEntry>);
|
||||
pub struct PortForwards(pub BTreeMap<SocketAddrV4, SocketAddrV4>);
|
||||
impl Map for PortForwards {
|
||||
type Key = SocketAddrV4;
|
||||
type Value = PortForwardEntry;
|
||||
type Value = SocketAddrV4;
|
||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
||||
Self::key_string(key)
|
||||
}
|
||||
|
||||
@@ -524,26 +524,26 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
||||
"To access your Web URL securely, trust your Root CA (displayed above) on your client device(s):\n",
|
||||
" - MacOS\n",
|
||||
" 1. Open the Terminal app\n",
|
||||
" 2. Type or copy/paste the following command (**DO NOT** click Enter/Return yet): pbpaste > ~/Desktop/tunnel-ca.crt\n",
|
||||
" 2. Paste the following command (**DO NOT** click Return): pbcopy < ~/Desktop/ca.crt\n",
|
||||
" 3. Copy your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
|
||||
" 4. Back in Terminal, click Enter/Return. tunnel-ca.crt is saved to your Desktop\n",
|
||||
" 5. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Mac\n",
|
||||
" 4. Back in Terminal, click Return. ca.crt is saved to your Desktop\n",
|
||||
" 5. Complete by trusting your Root CA: https://docs.start9.com/device-guides/mac/ca.html\n",
|
||||
" - Linux\n",
|
||||
" 1. Open gedit, nano, or any editor\n",
|
||||
" 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
|
||||
" 3. Name the file tunnel-ca.crt and save as plaintext\n",
|
||||
" 4. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Debian+%252F+Ubuntu\n",
|
||||
" 3. Name the file ca.crt and save as plaintext\n",
|
||||
" 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/linux/ca.html\n",
|
||||
" - Windows\n",
|
||||
" 1. Open the Notepad app\n",
|
||||
" 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
|
||||
" 3. Name the file tunnel-ca.crt and save as plaintext\n",
|
||||
" 4. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Windows\n",
|
||||
" 3. Name the file ca.crt and save as plaintext\n",
|
||||
" 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/windows/ca.html\n",
|
||||
" - Android/Graphene\n",
|
||||
" 1. Send the tunnel-ca.crt file (created above) to yourself\n",
|
||||
" 2. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Android+%252F+Graphene\n",
|
||||
" 1. Send the ca.crt file (created above) to yourself\n",
|
||||
" 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/android/ca.html\n",
|
||||
" - iOS\n",
|
||||
" 1. Send the tunnel-ca.crt file (created above) to yourself\n",
|
||||
" 2. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=iOS\n",
|
||||
" 1. Send the ca.crt file (created above) to yourself\n",
|
||||
" 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/ios/ca.html\n",
|
||||
));
|
||||
|
||||
return Ok(());
|
||||
|
||||
@@ -1,57 +1,41 @@
|
||||
import { SmtpValue } from '../../types'
|
||||
import { GetSystemSmtp, Patterns } from '../../util'
|
||||
import { InputSpec } from './builder/inputSpec'
|
||||
import { InputSpec, InputSpecOf } from './builder/inputSpec'
|
||||
import { Value } from './builder/value'
|
||||
import { Variants } from './builder/variants'
|
||||
|
||||
const securityVariants = Variants.of({
|
||||
tls: {
|
||||
name: 'TLS',
|
||||
spec: InputSpec.of({
|
||||
port: Value.dynamicText(async () => ({
|
||||
name: 'Port',
|
||||
required: true,
|
||||
default: '465',
|
||||
disabled: 'Fixed for TLS',
|
||||
})),
|
||||
}),
|
||||
},
|
||||
starttls: {
|
||||
name: 'STARTTLS',
|
||||
spec: InputSpec.of({
|
||||
port: Value.select({
|
||||
name: 'Port',
|
||||
default: '587',
|
||||
values: { '25': '25', '587': '587', '2525': '2525' },
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates an SMTP field spec with provider-specific defaults pre-filled.
|
||||
*/
|
||||
function smtpFields(
|
||||
defaults: {
|
||||
host?: string
|
||||
port?: number
|
||||
security?: 'starttls' | 'tls'
|
||||
hostDisabled?: boolean
|
||||
} = {},
|
||||
) {
|
||||
const hostSpec = Value.text({
|
||||
name: 'Host',
|
||||
required: true,
|
||||
default: defaults.host ?? null,
|
||||
placeholder: 'smtp.example.com',
|
||||
})
|
||||
|
||||
return InputSpec.of({
|
||||
host: defaults.hostDisabled
|
||||
? hostSpec.withDisabled('Fixed for this provider')
|
||||
: hostSpec,
|
||||
security: Value.union({
|
||||
): InputSpec<SmtpValue> {
|
||||
return InputSpec.of<InputSpecOf<SmtpValue>>({
|
||||
host: Value.text({
|
||||
name: 'Host',
|
||||
required: true,
|
||||
default: defaults.host ?? null,
|
||||
placeholder: 'smtp.example.com',
|
||||
}),
|
||||
port: Value.number({
|
||||
name: 'Port',
|
||||
required: true,
|
||||
default: defaults.port ?? 587,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
integer: true,
|
||||
}),
|
||||
security: Value.select({
|
||||
name: 'Connection Security',
|
||||
default: defaults.security ?? 'tls',
|
||||
variants: securityVariants,
|
||||
default: defaults.security ?? 'starttls',
|
||||
values: {
|
||||
starttls: 'STARTTLS',
|
||||
tls: 'TLS',
|
||||
},
|
||||
}),
|
||||
from: Value.text({
|
||||
name: 'From Address',
|
||||
@@ -88,39 +72,40 @@ export const smtpProviderVariants = Variants.of({
|
||||
name: 'Gmail',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.gmail.com',
|
||||
security: 'tls',
|
||||
hostDisabled: true,
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
ses: {
|
||||
name: 'Amazon SES',
|
||||
spec: smtpFields({
|
||||
host: 'email-smtp.us-east-1.amazonaws.com',
|
||||
security: 'tls',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
sendgrid: {
|
||||
name: 'SendGrid',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.sendgrid.net',
|
||||
security: 'tls',
|
||||
hostDisabled: true,
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
mailgun: {
|
||||
name: 'Mailgun',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.mailgun.org',
|
||||
security: 'tls',
|
||||
hostDisabled: true,
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
protonmail: {
|
||||
name: 'Proton Mail',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.protonmail.ch',
|
||||
security: 'tls',
|
||||
hostDisabled: true,
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
other: {
|
||||
@@ -136,7 +121,7 @@ export const smtpProviderVariants = Variants.of({
|
||||
export const systemSmtpSpec = InputSpec.of({
|
||||
provider: Value.union({
|
||||
name: 'Provider',
|
||||
default: 'gmail',
|
||||
default: null as any,
|
||||
variants: smtpProviderVariants,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -14,34 +14,28 @@ export const knownProtocols = {
|
||||
defaultPort: 80,
|
||||
withSsl: 'https',
|
||||
alpn: { specified: ['http/1.1'] } as AlpnInfo,
|
||||
addXForwardedHeaders: true,
|
||||
},
|
||||
https: {
|
||||
secure: { ssl: true },
|
||||
defaultPort: 443,
|
||||
addXForwardedHeaders: true,
|
||||
},
|
||||
ws: {
|
||||
secure: null,
|
||||
defaultPort: 80,
|
||||
withSsl: 'wss',
|
||||
alpn: { specified: ['http/1.1'] } as AlpnInfo,
|
||||
addXForwardedHeaders: true,
|
||||
},
|
||||
wss: {
|
||||
secure: { ssl: true },
|
||||
defaultPort: 443,
|
||||
addXForwardedHeaders: true,
|
||||
},
|
||||
ssh: {
|
||||
secure: { ssl: false },
|
||||
defaultPort: 22,
|
||||
addXForwardedHeaders: false,
|
||||
},
|
||||
dns: {
|
||||
secure: { ssl: false },
|
||||
defaultPort: 53,
|
||||
addXForwardedHeaders: false,
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -142,7 +136,7 @@ export class MultiHost {
|
||||
const sslProto = this.getSslProto(options)
|
||||
const addSsl = sslProto
|
||||
? {
|
||||
addXForwardedHeaders: knownProtocols[sslProto].addXForwardedHeaders,
|
||||
addXForwardedHeaders: false,
|
||||
preferredExternalPort: knownProtocols[sslProto].defaultPort,
|
||||
scheme: sslProto,
|
||||
alpn: 'alpn' in protoInfo ? protoInfo.alpn : null,
|
||||
@@ -154,7 +148,7 @@ export class MultiHost {
|
||||
preferredExternalPort: 443,
|
||||
scheme: sslProto,
|
||||
alpn: null,
|
||||
...options.addSsl,
|
||||
...('addSsl' in options ? options.addSsl : null),
|
||||
}
|
||||
: null
|
||||
|
||||
|
||||
@@ -6,5 +6,4 @@ export type AddPublicDomainParams = {
|
||||
fqdn: string
|
||||
acme: AcmeProvider | null
|
||||
gateway: GatewayId
|
||||
internalPort: number
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CheckPortRes } from './CheckPortRes'
|
||||
|
||||
export type AddPublicDomainRes = { dns: string | null; port: CheckPortRes }
|
||||
@@ -5,7 +5,6 @@ import type { DnsSettings } from './DnsSettings'
|
||||
import type { GatewayId } from './GatewayId'
|
||||
import type { Host } from './Host'
|
||||
import type { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
|
||||
import type { PassthroughInfo } from './PassthroughInfo'
|
||||
import type { WifiInfo } from './WifiInfo'
|
||||
|
||||
export type NetworkInfo = {
|
||||
@@ -15,5 +14,4 @@ export type NetworkInfo = {
|
||||
acme: { [key: AcmeProvider]: AcmeSettings }
|
||||
dns: DnsSettings
|
||||
defaultOutbound: string | null
|
||||
passthroughs: Array<PassthroughInfo>
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PassthroughInfo = {
|
||||
hostname: string
|
||||
listenPort: number
|
||||
backend: string
|
||||
publicGateways: string[]
|
||||
privateIps: string[]
|
||||
}
|
||||
@@ -19,7 +19,6 @@ export { AddPackageSignerParams } from './AddPackageSignerParams'
|
||||
export { AddPackageToCategoryParams } from './AddPackageToCategoryParams'
|
||||
export { AddPrivateDomainParams } from './AddPrivateDomainParams'
|
||||
export { AddPublicDomainParams } from './AddPublicDomainParams'
|
||||
export { AddPublicDomainRes } from './AddPublicDomainRes'
|
||||
export { AddressInfo } from './AddressInfo'
|
||||
export { AddSslOptions } from './AddSslOptions'
|
||||
export { AddTunnelParams } from './AddTunnelParams'
|
||||
@@ -202,7 +201,6 @@ export { PackagePlugin } from './PackagePlugin'
|
||||
export { PackageState } from './PackageState'
|
||||
export { PackageVersionInfo } from './PackageVersionInfo'
|
||||
export { PartitionInfo } from './PartitionInfo'
|
||||
export { PassthroughInfo } from './PassthroughInfo'
|
||||
export { PasswordType } from './PasswordType'
|
||||
export { PathOrUrl } from './PathOrUrl'
|
||||
export { Pem } from './Pem'
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
export * as inputSpecTypes from './actions/input/inputSpecTypes'
|
||||
export {
|
||||
CurrentDependenciesResult,
|
||||
OptionalDependenciesOf as OptionalDependencies,
|
||||
RequiredDependenciesOf as RequiredDependencies,
|
||||
} from './dependencies/setupDependencies'
|
||||
export * from './osBindings'
|
||||
export { SDKManifest } from './types/ManifestTypes'
|
||||
export { Effects }
|
||||
import { InputSpec as InputSpecClass } from './actions/input/builder/inputSpec'
|
||||
|
||||
import {
|
||||
DependencyRequirement,
|
||||
NamedHealthCheckResult,
|
||||
Manifest,
|
||||
ServiceInterface,
|
||||
ActionId,
|
||||
} from './osBindings'
|
||||
import { Affine, StringObject, ToKebab } from './util'
|
||||
import { Action, Actions } from './actions/setupActions'
|
||||
import { Effects } from './Effects'
|
||||
import { ExtendedVersion, VersionRange } from './exver'
|
||||
import {
|
||||
ActionId,
|
||||
DependencyRequirement,
|
||||
Manifest,
|
||||
NamedHealthCheckResult,
|
||||
ServiceInterface,
|
||||
} from './osBindings'
|
||||
import { StringObject, ToKebab } from './util'
|
||||
export { Effects }
|
||||
export * from './osBindings'
|
||||
export { SDKManifest } from './types/ManifestTypes'
|
||||
export {
|
||||
RequiredDependenciesOf as RequiredDependencies,
|
||||
OptionalDependenciesOf as OptionalDependencies,
|
||||
CurrentDependenciesResult,
|
||||
} from './dependencies/setupDependencies'
|
||||
|
||||
/** An object that can be built into a terminable daemon process. */
|
||||
export type DaemonBuildable = {
|
||||
|
||||
@@ -26,18 +26,6 @@ export const getHostname = (url: string): Hostname | null => {
|
||||
return last
|
||||
}
|
||||
|
||||
/**
|
||||
* The kinds of hostnames that can be filtered on.
|
||||
*
|
||||
* - `'mdns'` — mDNS / Bonjour `.local` hostnames
|
||||
* - `'domain'` — any os-managed domain name (matches both `'private-domain'` and `'public-domain'` metadata kinds)
|
||||
* - `'ip'` — shorthand for both `'ipv4'` and `'ipv6'`
|
||||
* - `'ipv4'` — IPv4 addresses only
|
||||
* - `'ipv6'` — IPv6 addresses only
|
||||
* - `'localhost'` — loopback addresses (`localhost`, `127.0.0.1`, `::1`)
|
||||
* - `'link-local'` — IPv6 link-local addresses (fe80::/10)
|
||||
* - `'plugin'` — hostnames provided by a plugin package
|
||||
*/
|
||||
type FilterKinds =
|
||||
| 'mdns'
|
||||
| 'domain'
|
||||
@@ -46,25 +34,10 @@ type FilterKinds =
|
||||
| 'ipv6'
|
||||
| 'localhost'
|
||||
| 'link-local'
|
||||
| 'plugin'
|
||||
|
||||
/**
|
||||
* Describes which hostnames to include (or exclude) when filtering a `Filled` address.
|
||||
*
|
||||
* Every field is optional — omitted fields impose no constraint.
|
||||
* Filters are composable: the `.filter()` method intersects successive filters,
|
||||
* and the `exclude` field inverts a nested filter.
|
||||
*/
|
||||
export type Filter = {
|
||||
/** Keep only hostnames with the given visibility. `'public'` = externally reachable, `'private'` = LAN-only. */
|
||||
visibility?: 'public' | 'private'
|
||||
/** Keep only hostnames whose metadata kind matches. A single kind or array of kinds. `'ip'` expands to `['ipv4','ipv6']`, `'domain'` matches both `'private-domain'` and `'public-domain'`. */
|
||||
kind?: FilterKinds | FilterKinds[]
|
||||
/** Arbitrary predicate — hostnames for which this returns `false` are excluded. */
|
||||
predicate?: (h: HostnameInfo) => boolean
|
||||
/** Keep only plugin hostnames provided by this package. Implies `kind: 'plugin'`. */
|
||||
pluginId?: PackageId
|
||||
/** A nested filter whose matches are *removed* from the result (logical NOT). */
|
||||
exclude?: Filter
|
||||
}
|
||||
|
||||
@@ -92,13 +65,9 @@ type KindFilter<K extends FilterKinds> = K extends 'mdns'
|
||||
?
|
||||
| (HostnameInfo & { metadata: { kind: 'ipv6' } })
|
||||
| KindFilter<Exclude<K, 'ipv6'>>
|
||||
: K extends 'plugin'
|
||||
?
|
||||
| (HostnameInfo & { metadata: { kind: 'plugin' } })
|
||||
| KindFilter<Exclude<K, 'plugin'>>
|
||||
: K extends 'ip'
|
||||
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
||||
: never
|
||||
: K extends 'ip'
|
||||
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
||||
: never
|
||||
|
||||
type FilterReturnTy<F extends Filter> = F extends {
|
||||
visibility: infer V extends 'public' | 'private'
|
||||
@@ -138,62 +107,20 @@ type FormatReturnTy<
|
||||
? UrlString | FormatReturnTy<F, Exclude<Format, 'urlstring'>>
|
||||
: never
|
||||
|
||||
/**
|
||||
* A resolved address with its hostnames already populated, plus helpers
|
||||
* for filtering, formatting, and converting hostnames to URLs.
|
||||
*
|
||||
* Filters are chainable and each call returns a new `Filled` narrowed to the
|
||||
* matching subset of hostnames:
|
||||
*
|
||||
* ```ts
|
||||
* addresses.nonLocal // exclude localhost & link-local
|
||||
* addresses.public // only publicly-reachable hostnames
|
||||
* addresses.filter({ kind: 'domain' }) // only domain-name hostnames
|
||||
* addresses.filter({ visibility: 'private' }) // only LAN-reachable hostnames
|
||||
* addresses.nonLocal.filter({ kind: 'ip' }) // chainable — non-local IPs only
|
||||
* ```
|
||||
*/
|
||||
export type Filled<F extends Filter = {}> = {
|
||||
/** The hostnames that survived all applied filters. */
|
||||
hostnames: HostnameInfo[]
|
||||
|
||||
/** Convert a single hostname into a fully-formed URL string, applying the address's scheme, username, and suffix. */
|
||||
toUrl: (h: HostnameInfo) => UrlString
|
||||
|
||||
/**
|
||||
* Return every hostname in the requested format.
|
||||
*
|
||||
* - `'urlstring'` (default) — formatted URL strings
|
||||
* - `'url'` — `URL` objects
|
||||
* - `'hostname-info'` — raw `HostnameInfo` objects
|
||||
*/
|
||||
format: <Format extends Formats = 'urlstring'>(
|
||||
format?: Format,
|
||||
) => FormatReturnTy<{}, Format>[]
|
||||
|
||||
/**
|
||||
* Apply an arbitrary {@link Filter} and return a new `Filled` containing only
|
||||
* the hostnames that match. Filters compose: calling `.filter()` on an
|
||||
* already-filtered `Filled` intersects the constraints.
|
||||
*/
|
||||
filter: <NewFilter extends Filter>(
|
||||
filter: NewFilter,
|
||||
) => Filled<NewFilter & Filter>
|
||||
|
||||
/**
|
||||
* Apply multiple filters and return hostnames that match **any** of them (union / OR).
|
||||
*
|
||||
* ```ts
|
||||
* addresses.matchesAny([{ kind: 'domain' }, { kind: 'mdns' }])
|
||||
* ```
|
||||
*/
|
||||
matchesAny: <NewFilters extends Filter[]>(
|
||||
filters: [...NewFilters],
|
||||
) => Filled<NewFilters[number] & F>
|
||||
|
||||
/** Shorthand filter that excludes `localhost` and IPv6 link-local addresses — keeps only network-reachable hostnames. */
|
||||
nonLocal: Filled<typeof nonLocalFilter & Filter>
|
||||
/** Shorthand filter that keeps only publicly-reachable hostnames (those with `public: true`). */
|
||||
public: Filled<typeof publicFilter & Filter>
|
||||
}
|
||||
export type FilledAddressInfo = AddressInfo & Filled
|
||||
@@ -283,16 +210,7 @@ function filterRec(
|
||||
['localhost', '127.0.0.1', '::1'].includes(h.hostname)) ||
|
||||
(kind.has('link-local') &&
|
||||
h.metadata.kind === 'ipv6' &&
|
||||
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname))) ||
|
||||
(kind.has('plugin') && h.metadata.kind === 'plugin')),
|
||||
)
|
||||
}
|
||||
if (filter.pluginId) {
|
||||
const id = filter.pluginId
|
||||
hostnames = hostnames.filter(
|
||||
(h) =>
|
||||
invert !==
|
||||
(h.metadata.kind === 'plugin' && h.metadata.packageId === id),
|
||||
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname)))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -324,14 +242,6 @@ function enabledAddresses(addr: DerivedAddressInfo): HostnameInfo[] {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out localhost and IPv6 link-local hostnames from a list.
|
||||
* Equivalent to the `nonLocal` filter on `Filled` addresses.
|
||||
*/
|
||||
export function filterNonLocal(hostnames: HostnameInfo[]): HostnameInfo[] {
|
||||
return filterRec(hostnames, nonLocalFilter, false)
|
||||
}
|
||||
|
||||
export const filledAddress = (
|
||||
host: Host,
|
||||
addressInfo: AddressInfo,
|
||||
@@ -370,19 +280,6 @@ export const filledAddress = (
|
||||
filterRec(hostnames, filter, false),
|
||||
)
|
||||
},
|
||||
matchesAny: <NewFilters extends Filter[]>(filters: [...NewFilters]) => {
|
||||
const seen = new Set<HostnameInfo>()
|
||||
const union: HostnameInfo[] = []
|
||||
for (const f of filters) {
|
||||
for (const h of filterRec(hostnames, f, false)) {
|
||||
if (!seen.has(h)) {
|
||||
seen.add(h)
|
||||
union.push(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filledAddressFromHostnames<NewFilters[number] & F>(union)
|
||||
},
|
||||
get nonLocal(): Filled<typeof nonLocalFilter & F> {
|
||||
return getNonLocal()
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ export {
|
||||
GetServiceInterface,
|
||||
getServiceInterface,
|
||||
filledAddress,
|
||||
filterNonLocal,
|
||||
} from './getServiceInterface'
|
||||
export { getServiceInterfaces } from './getServiceInterfaces'
|
||||
export { once } from './once'
|
||||
|
||||
@@ -141,7 +141,6 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
| 'getSystemSmtp'
|
||||
| 'getOutboundGateway'
|
||||
| 'getContainerIp'
|
||||
| 'getStatus'
|
||||
| 'getDataVersion'
|
||||
| 'setDataVersion'
|
||||
| 'getServiceManifest'
|
||||
@@ -165,6 +164,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
getSslKey: (effects, ...args) => effects.getSslKey(...args),
|
||||
shutdown: (effects, ...args) => effects.shutdown(...args),
|
||||
getDependencies: (effects, ...args) => effects.getDependencies(...args),
|
||||
getStatus: (effects, ...args) => effects.getStatus(...args),
|
||||
setHealth: (effects, ...args) => effects.setHealth(...args),
|
||||
}
|
||||
|
||||
@@ -342,104 +342,6 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the service's current status with reactive subscription support.
|
||||
*
|
||||
* Returns an object with multiple read strategies: `const()` for a value
|
||||
* that retries on change, `once()` for a single read, `watch()` for an async
|
||||
* generator, `onChange()` for a callback, and `waitFor()` to block until a predicate is met.
|
||||
*
|
||||
* @param effects - The effects context
|
||||
* @param options - Optional filtering options (e.g. `packageId`)
|
||||
*/
|
||||
getStatus: (
|
||||
effects: T.Effects,
|
||||
options: Omit<Parameters<T.Effects['getStatus']>[0], 'callback'> = {},
|
||||
) => {
|
||||
async function* watch(abort?: AbortSignal) {
|
||||
const resolveCell = { resolve: () => {} }
|
||||
effects.onLeaveContext(() => {
|
||||
resolveCell.resolve()
|
||||
})
|
||||
abort?.addEventListener('abort', () => resolveCell.resolve())
|
||||
while (effects.isInContext && !abort?.aborted) {
|
||||
let callback: () => void = () => {}
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
callback = resolve
|
||||
resolveCell.resolve = resolve
|
||||
})
|
||||
yield await effects.getStatus({ ...options, callback })
|
||||
await waitForNext
|
||||
}
|
||||
}
|
||||
return {
|
||||
const: () =>
|
||||
effects.getStatus({
|
||||
...options,
|
||||
callback:
|
||||
effects.constRetry &&
|
||||
(() => effects.constRetry && effects.constRetry()),
|
||||
}),
|
||||
once: () => effects.getStatus(options),
|
||||
watch: (abort?: AbortSignal) => {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
return DropGenerator.of(watch(ctrl.signal), () => ctrl.abort())
|
||||
},
|
||||
onChange: (
|
||||
callback: (
|
||||
value: T.StatusInfo | null,
|
||||
error?: Error,
|
||||
) => { cancel: boolean } | Promise<{ cancel: boolean }>,
|
||||
) => {
|
||||
;(async () => {
|
||||
const ctrl = new AbortController()
|
||||
for await (const value of watch(ctrl.signal)) {
|
||||
try {
|
||||
const res = await callback(value)
|
||||
if (res.cancel) {
|
||||
ctrl.abort()
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'callback function threw an error @ getStatus.onChange',
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
})()
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
'callback function threw an error @ getStatus.onChange',
|
||||
e,
|
||||
),
|
||||
)
|
||||
},
|
||||
waitFor: async (pred: (value: T.StatusInfo | null) => boolean) => {
|
||||
const resolveCell = { resolve: () => {} }
|
||||
effects.onLeaveContext(() => {
|
||||
resolveCell.resolve()
|
||||
})
|
||||
while (effects.isInContext) {
|
||||
let callback: () => void = () => {}
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
callback = resolve
|
||||
resolveCell.resolve = resolve
|
||||
})
|
||||
const res = await effects.getStatus({ ...options, callback })
|
||||
if (pred(res)) {
|
||||
resolveCell.resolve()
|
||||
return res
|
||||
}
|
||||
await waitForNext
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
MultiHost: {
|
||||
/**
|
||||
* Create a new MultiHost instance for binding ports and exporting interfaces.
|
||||
|
||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.58",
|
||||
"version": "0.4.0-beta.55",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.58",
|
||||
"version": "0.4.0-beta.55",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.58",
|
||||
"version": "0.4.0-beta.55",
|
||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||
"main": "./package/lib/index.js",
|
||||
"types": "./package/lib/index.d.ts",
|
||||
|
||||
2502
web/package-lock.json
generated
2502
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,33 +33,34 @@
|
||||
"format:check": "prettier --check projects/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.2.1",
|
||||
"@angular/cdk": "^21.2.1",
|
||||
"@angular/common": "^21.2.1",
|
||||
"@angular/compiler": "^21.2.1",
|
||||
"@angular/core": "^21.2.1",
|
||||
"@angular/forms": "^21.2.1",
|
||||
"@angular/platform-browser": "^21.2.1",
|
||||
"@angular/pwa": "^21.2.1",
|
||||
"@angular/router": "^21.2.1",
|
||||
"@angular/service-worker": "^21.2.1",
|
||||
"@angular/animations": "^20.3.0",
|
||||
"@angular/cdk": "^20.1.0",
|
||||
"@angular/common": "^20.3.0",
|
||||
"@angular/compiler": "^20.3.0",
|
||||
"@angular/core": "^20.3.0",
|
||||
"@angular/forms": "^20.3.0",
|
||||
"@angular/platform-browser": "^20.3.0",
|
||||
"@angular/platform-browser-dynamic": "^20.1.0",
|
||||
"@angular/pwa": "^20.3.0",
|
||||
"@angular/router": "^20.3.0",
|
||||
"@angular/service-worker": "^20.3.0",
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/argon2": "^0.3.0",
|
||||
"@start9labs/start-sdk": "file:../sdk/baseDist",
|
||||
"@taiga-ui/addon-charts": "4.73.0",
|
||||
"@taiga-ui/addon-commerce": "4.73.0",
|
||||
"@taiga-ui/addon-mobile": "4.73.0",
|
||||
"@taiga-ui/addon-table": "4.73.0",
|
||||
"@taiga-ui/cdk": "4.73.0",
|
||||
"@taiga-ui/core": "4.73.0",
|
||||
"@taiga-ui/addon-charts": "4.66.0",
|
||||
"@taiga-ui/addon-commerce": "4.66.0",
|
||||
"@taiga-ui/addon-mobile": "4.66.0",
|
||||
"@taiga-ui/addon-table": "4.66.0",
|
||||
"@taiga-ui/cdk": "4.66.0",
|
||||
"@taiga-ui/core": "4.66.0",
|
||||
"@taiga-ui/dompurify": "4.1.11",
|
||||
"@taiga-ui/event-plugins": "4.7.0",
|
||||
"@taiga-ui/experimental": "4.73.0",
|
||||
"@taiga-ui/icons": "4.73.0",
|
||||
"@taiga-ui/kit": "4.73.0",
|
||||
"@taiga-ui/layout": "4.73.0",
|
||||
"@taiga-ui/experimental": "4.66.0",
|
||||
"@taiga-ui/icons": "4.66.0",
|
||||
"@taiga-ui/kit": "4.66.0",
|
||||
"@taiga-ui/layout": "4.66.0",
|
||||
"@taiga-ui/polymorpheus": "4.9.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -79,7 +80,7 @@
|
||||
"mime": "^4.0.3",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"mustache": "^4.2.0",
|
||||
"ng-qrcode": "^21.0.0",
|
||||
"ng-qrcode": "^20.0.0",
|
||||
"node-jose": "^2.2.0",
|
||||
"patch-db-client": "file:../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
@@ -91,10 +92,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-experts/hawkeye": "^1.7.2",
|
||||
"@angular/build": "^21.2.1",
|
||||
"@angular/cli": "^21.2.1",
|
||||
"@angular/compiler-cli": "^21.2.1",
|
||||
"@angular/language-service": "^21.2.1",
|
||||
"@angular/build": "^20.1.0",
|
||||
"@angular/cli": "^20.1.0",
|
||||
"@angular/compiler-cli": "^20.1.0",
|
||||
"@angular/language-service": "^20.1.0",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/estree": "^0.0.51",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
@@ -106,7 +107,7 @@
|
||||
"@types/uuid": "^8.3.1",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^13.2.0",
|
||||
"ng-packagr": "^21.2.0",
|
||||
"ng-packagr": "^20.1.0",
|
||||
"node-html-parser": "^5.3.3",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^3.5.3",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import {
|
||||
DocsLinkDirective,
|
||||
i18nPipe,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
TuiLoader,
|
||||
TuiPopup,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiDrawer, TuiSkeleton } from '@taiga-ui/kit'
|
||||
import { CategoriesModule } from '../../pages/list/categories/categories.module'
|
||||
import { SearchModule } from '../../pages/list/search/search.module'
|
||||
import { StoreIconComponentModule } from '../store-icon/store-icon.component.module'
|
||||
import { MenuComponent } from './menu.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedPipesModule,
|
||||
SearchModule,
|
||||
CategoriesModule,
|
||||
TuiLoader,
|
||||
TuiButton,
|
||||
CategoriesModule,
|
||||
StoreIconComponentModule,
|
||||
TuiAppearance,
|
||||
TuiIcon,
|
||||
TuiSkeleton,
|
||||
TuiDrawer,
|
||||
TuiPopup,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
],
|
||||
declarations: [MenuComponent],
|
||||
exports: [MenuComponent],
|
||||
})
|
||||
export class MenuModule {}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -7,35 +6,16 @@ import {
|
||||
OnDestroy,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiAppearance, TuiButton, TuiIcon, TuiPopup } from '@taiga-ui/core'
|
||||
import { TuiDrawer, TuiSkeleton } from '@taiga-ui/kit'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { CategoriesComponent } from '../../pages/list/categories/categories.component'
|
||||
import { SearchComponent } from '../../pages/list/search/search.component'
|
||||
import { AbstractCategoryService } from '../../services/category.service'
|
||||
import { StoreDataWithUrl } from '../../types'
|
||||
import { StoreIconComponent } from '../store-icon.component'
|
||||
|
||||
@Component({
|
||||
selector: 'menu',
|
||||
templateUrl: './menu.component.html',
|
||||
styleUrls: ['./menu.component.scss'],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchComponent,
|
||||
CategoriesComponent,
|
||||
TuiButton,
|
||||
StoreIconComponent,
|
||||
TuiAppearance,
|
||||
TuiIcon,
|
||||
TuiSkeleton,
|
||||
TuiDrawer,
|
||||
TuiPopup,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class MenuComponent implements OnDestroy {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { StoreIconComponent } from './store-icon.component'
|
||||
import { StoreIconComponentModule } from './store-icon/store-icon.component.module'
|
||||
|
||||
@Component({
|
||||
selector: '[registry]',
|
||||
@@ -17,7 +17,7 @@ import { StoreIconComponent } from './store-icon.component'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [StoreIconComponent, TuiIcon, TuiTitle],
|
||||
imports: [StoreIconComponentModule, TuiIcon, TuiTitle],
|
||||
})
|
||||
export class MarketplaceRegistryComponent {
|
||||
@Input()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { StoreIconComponent } from './store-icon.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [StoreIconComponent],
|
||||
imports: [CommonModule],
|
||||
exports: [StoreIconComponent],
|
||||
})
|
||||
export class StoreIconComponentModule {}
|
||||
@@ -21,6 +21,7 @@ import { knownRegistries, sameUrl } from '@start9labs/shared'
|
||||
`,
|
||||
styles: ':host { overflow: hidden; }',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class StoreIconComponent {
|
||||
@Input()
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -6,11 +5,7 @@ import {
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { LocalizePipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiAppearance, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||
|
||||
const ICONS: Record<string, string> = {
|
||||
all: '@tui.layout-grid',
|
||||
@@ -31,15 +26,8 @@ const ICONS: Record<string, string> = {
|
||||
selector: 'marketplace-categories',
|
||||
templateUrl: 'categories.component.html',
|
||||
styleUrls: ['categories.component.scss'],
|
||||
imports: [
|
||||
RouterModule,
|
||||
CommonModule,
|
||||
TuiAppearance,
|
||||
TuiIcon,
|
||||
TuiSkeleton,
|
||||
LocalizePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class CategoriesComponent {
|
||||
@Input()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { TuiIcon, TuiAppearance } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||
import { LocalizePipe } from '@start9labs/shared'
|
||||
|
||||
import { CategoriesComponent } from './categories.component'
|
||||
import { RouterModule } from '@angular/router'
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule, CommonModule, TuiAppearance, TuiIcon, TuiSkeleton, LocalizePipe],
|
||||
declarations: [CategoriesComponent],
|
||||
exports: [CategoriesComponent],
|
||||
})
|
||||
export class CategoriesModule {}
|
||||
@@ -1,15 +1,12 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { LocalizePipe, TickerComponent } from '@start9labs/shared'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-item',
|
||||
templateUrl: 'item.component.html',
|
||||
styleUrls: ['item.component.scss'],
|
||||
imports: [CommonModule, RouterModule, TickerComponent, LocalizePipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ItemComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
12
web/projects/marketplace/src/pages/list/item/item.module.ts
Normal file
12
web/projects/marketplace/src/pages/list/item/item.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { LocalizePipe, SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
import { ItemComponent } from './item.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ItemComponent],
|
||||
exports: [ItemComponent],
|
||||
imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent, LocalizePipe],
|
||||
})
|
||||
export class ItemModule {}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -6,15 +5,13 @@ import {
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TuiIcon } from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-search',
|
||||
templateUrl: 'search.component.html',
|
||||
styleUrls: ['search.component.scss'],
|
||||
imports: [FormsModule, CommonModule, TuiIcon],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class SearchComponent {
|
||||
@Input()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TuiIcon } from '@taiga-ui/core'
|
||||
import { SearchComponent } from './search.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [FormsModule, CommonModule, TuiIcon],
|
||||
declarations: [SearchComponent],
|
||||
exports: [SearchComponent],
|
||||
})
|
||||
export class SearchModule {}
|
||||
@@ -1,12 +1,7 @@
|
||||
import { KeyValue } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, inject, Input } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { i18nPipe, i18nService } from '@start9labs/shared'
|
||||
import { ExverPipesModule, i18nPipe, i18nService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
@@ -25,7 +20,9 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
<tui-line-clamp [linesLimit]="2" [content]="titleContent" />
|
||||
<ng-template #titleContent>
|
||||
<div class="title">
|
||||
<span>{{ getTitle(dep.key) }}</span>
|
||||
<span>
|
||||
{{ getTitle(dep.key) }}
|
||||
</span>
|
||||
<p>
|
||||
@if (dep.value.optional) {
|
||||
<span>({{ 'Optional' | i18n }})</span>
|
||||
@@ -40,7 +37,9 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
[content]="descContent"
|
||||
class="description"
|
||||
/>
|
||||
<ng-template #descContent>{{ dep.value.description }}</ng-template>
|
||||
<ng-template #descContent>
|
||||
{{ dep.value.description }}
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
@@ -95,7 +94,7 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterModule, TuiAvatar, TuiLineClamp, i18nPipe],
|
||||
imports: [RouterModule, TuiAvatar, ExverPipesModule, TuiLineClamp, i18nPipe],
|
||||
})
|
||||
export class MarketplaceDepItemComponent {
|
||||
private readonly i18nService = inject(i18nService)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { i18nPipe, TrustUrlPipe } from '@start9labs/shared'
|
||||
import { i18nPipe, SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiAvatar } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
@@ -47,7 +47,14 @@ import { MarketplacePkg } from '../../types'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterLink, TuiCell, TuiTitle, TrustUrlPipe, TuiAvatar, i18nPipe],
|
||||
imports: [
|
||||
RouterLink,
|
||||
TuiCell,
|
||||
TuiTitle,
|
||||
SharedPipesModule,
|
||||
TuiAvatar,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export class MarketplaceFlavorsComponent {
|
||||
@Input()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TickerComponent } from '@start9labs/shared'
|
||||
import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
@@ -118,7 +118,7 @@ import { T } from '@start9labs/start-sdk'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TickerComponent],
|
||||
imports: [SharedPipesModule, TickerComponent],
|
||||
})
|
||||
export class MarketplacePackageHeroComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TemplateRef,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { DialogService, i18nPipe } from '@start9labs/shared'
|
||||
import { DialogService, i18nPipe, SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { TuiRadioList } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
@@ -76,6 +76,7 @@ import { MarketplaceItemComponent } from './item.component'
|
||||
imports: [
|
||||
MarketplaceItemComponent,
|
||||
TuiButton,
|
||||
SharedPipesModule,
|
||||
FormsModule,
|
||||
TuiRadioList,
|
||||
i18nPipe,
|
||||
|
||||
@@ -4,6 +4,7 @@ import Fuse from 'fuse.js'
|
||||
|
||||
@Pipe({
|
||||
name: 'filterPackages',
|
||||
standalone: false,
|
||||
})
|
||||
export class FilterPackagesPipe implements PipeTransform {
|
||||
transform(
|
||||
@@ -78,3 +79,9 @@ export class FilterPackagesPipe implements PipeTransform {
|
||||
.map(a => ({ ...a }))
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [FilterPackagesPipe],
|
||||
exports: [FilterPackagesPipe],
|
||||
})
|
||||
export class FilterPackagesPipeModule {}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
*/
|
||||
|
||||
export * from './pages/list/categories/categories.component'
|
||||
export * from './pages/list/categories/categories.module'
|
||||
export * from './pages/list/item/item.component'
|
||||
export * from './pages/list/item/item.module'
|
||||
export * from './pages/list/search/search.component'
|
||||
export * from './pages/list/search/search.module'
|
||||
export * from './pages/show/link.component'
|
||||
export * from './pages/show/item.component'
|
||||
export * from './pages/show/links.component'
|
||||
@@ -19,7 +22,10 @@ export * from './pages/show/release-notes.component'
|
||||
|
||||
export * from './pipes/filter-packages.pipe'
|
||||
|
||||
export * from './components/store-icon.component'
|
||||
export * from './components/store-icon/store-icon.component'
|
||||
export * from './components/store-icon/store-icon.component.module'
|
||||
export * from './components/store-icon/store-icon.component'
|
||||
export * from './components/menu/menu.component.module'
|
||||
export * from './components/menu/menu.component'
|
||||
export * from './components/registry.component'
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Component, DOCUMENT, inject, OnInit } from '@angular/core'
|
||||
import { Router, RouterOutlet } from '@angular/router'
|
||||
import { Component, inject, DOCUMENT } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { TuiRoot } from '@taiga-ui/core'
|
||||
|
||||
import { ApiService } from './services/api.service'
|
||||
import { StateService } from './services/state.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<tui-root tuiTheme="dark"><router-outlet /></tui-root>',
|
||||
imports: [TuiRoot, RouterOutlet],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
export class AppComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly router = inject(Router)
|
||||
|
||||
@@ -3,20 +3,9 @@ import {
|
||||
withFetch,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import {
|
||||
ApplicationConfig,
|
||||
inject,
|
||||
provideAppInitializer,
|
||||
provideZoneChangeDetection,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { provideAnimations } from '@angular/platform-browser/animations'
|
||||
import {
|
||||
PreloadAllModules,
|
||||
provideRouter,
|
||||
withDisabledInitialNavigation,
|
||||
withPreloading,
|
||||
} from '@angular/router'
|
||||
import { inject, NgModule, provideAppInitializer } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { PreloadAllModules, RouterModule } from '@angular/router'
|
||||
import { WA_LOCATION } from '@ng-web-apis/common'
|
||||
import initArgon from '@start9labs/argon2'
|
||||
import {
|
||||
@@ -26,16 +15,13 @@ import {
|
||||
VERSION,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
tuiButtonOptionsProvider,
|
||||
tuiTextfieldOptionsProvider,
|
||||
} from '@taiga-ui/core'
|
||||
import { provideEventPlugins } from '@taiga-ui/event-plugins'
|
||||
|
||||
import { ROUTES } from './app.routes'
|
||||
import { tuiButtonOptionsProvider, TuiRoot } from '@taiga-ui/core'
|
||||
import { NG_EVENT_PLUGINS } from '@taiga-ui/event-plugins'
|
||||
import { ApiService } from './services/api.service'
|
||||
import { LiveApiService } from './services/live-api.service'
|
||||
import { MockApiService } from './services/mock-api.service'
|
||||
import { AppComponent } from './app.component'
|
||||
import { ROUTES } from './app.routes'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
@@ -44,16 +30,18 @@ const {
|
||||
|
||||
const version = require('../../../../package.json').version
|
||||
|
||||
export const APP_CONFIG: ApplicationConfig = {
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
RouterModule.forRoot(ROUTES, {
|
||||
preloadingStrategy: PreloadAllModules,
|
||||
initialNavigation: 'disabled',
|
||||
}),
|
||||
TuiRoot,
|
||||
],
|
||||
providers: [
|
||||
provideZoneChangeDetection(),
|
||||
provideAnimations(),
|
||||
provideEventPlugins(),
|
||||
provideRouter(
|
||||
ROUTES,
|
||||
withDisabledInitialNavigation(),
|
||||
withPreloading(PreloadAllModules),
|
||||
),
|
||||
NG_EVENT_PLUGINS,
|
||||
I18N_PROVIDERS,
|
||||
provideSetupLogsService(ApiService),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
@@ -76,6 +64,7 @@ export const APP_CONFIG: ApplicationConfig = {
|
||||
|
||||
initArgon({ module_or_path })
|
||||
}),
|
||||
tuiTextfieldOptionsProvider({ cleaner: signal(false) }),
|
||||
],
|
||||
}
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -5,6 +5,7 @@ import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [TuiButton, i18nPipe],
|
||||
template: `
|
||||
<p>{{ 'This drive contains existing StartOS data.' | i18n }}</p>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [TuiButton, i18nPipe],
|
||||
template: `
|
||||
<div class="animation-container">
|
||||
|
||||
@@ -11,6 +11,7 @@ interface Data {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [FormsModule, TuiTextfield, TuiSelect, TuiDataListWrapper, i18nPipe],
|
||||
template: `
|
||||
<p>{{ 'Multiple backups found. Select which one to restore.' | i18n }}</p>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TuiPassword } from '@taiga-ui/kit'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiButton,
|
||||
|
||||
@@ -34,121 +34,110 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
|
||||
@Component({
|
||||
template: `
|
||||
@if (!shuttingDown) {
|
||||
<section tuiCardLarge="compact">
|
||||
<header tuiHeader>
|
||||
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
||||
</header>
|
||||
<section tuiCardLarge="compact">
|
||||
<header tuiHeader>
|
||||
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
||||
</header>
|
||||
|
||||
@if (loading) {
|
||||
<tui-loader />
|
||||
} @else if (drives.length === 0) {
|
||||
<p class="no-drives">
|
||||
{{
|
||||
'No drives found. Please connect a drive and click Refresh.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
} @else {
|
||||
<tui-textfield
|
||||
[stringify]="stringify"
|
||||
[disabledItemHandler]="osDisabled"
|
||||
>
|
||||
<label tuiLabel>{{ 'OS Drive' | i18n }}</label>
|
||||
@if (mobile) {
|
||||
<select
|
||||
tuiSelect
|
||||
[ngModel]="selectedOsDrive"
|
||||
(ngModelChange)="onOsDriveChange($event)"
|
||||
[items]="drives"
|
||||
></select>
|
||||
} @else {
|
||||
<input
|
||||
tuiSelect
|
||||
[ngModel]="selectedOsDrive"
|
||||
(ngModelChange)="onOsDriveChange($event)"
|
||||
/>
|
||||
}
|
||||
@if (!mobile) {
|
||||
<tui-data-list-wrapper
|
||||
new
|
||||
*tuiTextfieldDropdown
|
||||
[items]="drives"
|
||||
[itemContent]="driveContent"
|
||||
/>
|
||||
}
|
||||
<tui-icon [tuiTooltip]="osDriveTooltip" />
|
||||
</tui-textfield>
|
||||
|
||||
<tui-textfield
|
||||
[stringify]="stringify"
|
||||
[disabledItemHandler]="dataDisabled"
|
||||
>
|
||||
<label tuiLabel>{{ 'Data Drive' | i18n }}</label>
|
||||
@if (mobile) {
|
||||
<select
|
||||
tuiSelect
|
||||
[(ngModel)]="selectedDataDrive"
|
||||
(ngModelChange)="onDataDriveChange($event)"
|
||||
[items]="drives"
|
||||
></select>
|
||||
} @else {
|
||||
<input
|
||||
tuiSelect
|
||||
[(ngModel)]="selectedDataDrive"
|
||||
(ngModelChange)="onDataDriveChange($event)"
|
||||
/>
|
||||
}
|
||||
@if (!mobile) {
|
||||
<tui-data-list-wrapper
|
||||
new
|
||||
*tuiTextfieldDropdown
|
||||
[items]="drives"
|
||||
[itemContent]="driveContent"
|
||||
/>
|
||||
}
|
||||
@if (preserveData === true) {
|
||||
<tui-icon
|
||||
icon="@tui.database"
|
||||
style="color: var(--tui-status-positive); pointer-events: none"
|
||||
/>
|
||||
}
|
||||
@if (preserveData === false) {
|
||||
<tui-icon
|
||||
icon="@tui.database-zap"
|
||||
style="color: var(--tui-status-negative); pointer-events: none"
|
||||
/>
|
||||
}
|
||||
<tui-icon [tuiTooltip]="dataDriveTooltip" />
|
||||
</tui-textfield>
|
||||
|
||||
<ng-template #driveContent let-drive>
|
||||
<div class="drive-item">
|
||||
<span class="drive-name">
|
||||
{{ driveName(drive) }}
|
||||
</span>
|
||||
<small>
|
||||
{{ formatCapacity(drive.capacity) }} · {{ drive.logicalname }}
|
||||
</small>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<footer>
|
||||
@if (drives.length === 0) {
|
||||
<button tuiButton appearance="secondary" (click)="refresh()">
|
||||
{{ 'Refresh' | i18n }}
|
||||
</button>
|
||||
@if (loading) {
|
||||
<tui-loader />
|
||||
} @else if (drives.length === 0) {
|
||||
<p class="no-drives">
|
||||
{{
|
||||
'No drives found. Please connect a drive and click Refresh.' | i18n
|
||||
}}
|
||||
</p>
|
||||
} @else {
|
||||
<tui-textfield [stringify]="stringify">
|
||||
<label tuiLabel>{{ 'OS Drive' | i18n }}</label>
|
||||
@if (mobile) {
|
||||
<select
|
||||
tuiSelect
|
||||
[(ngModel)]="selectedOsDrive"
|
||||
[items]="drives"
|
||||
></select>
|
||||
} @else {
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!selectedOsDrive || !selectedDataDrive"
|
||||
(click)="continue()"
|
||||
>
|
||||
{{ 'Continue' | i18n }}
|
||||
</button>
|
||||
<input tuiSelect [(ngModel)]="selectedOsDrive" />
|
||||
}
|
||||
</footer>
|
||||
</section>
|
||||
@if (!mobile) {
|
||||
<tui-data-list-wrapper
|
||||
new
|
||||
*tuiTextfieldDropdown
|
||||
[items]="drives"
|
||||
[itemContent]="driveContent"
|
||||
/>
|
||||
}
|
||||
<tui-icon [tuiTooltip]="osDriveTooltip" />
|
||||
</tui-textfield>
|
||||
|
||||
<tui-textfield [stringify]="stringify">
|
||||
<label tuiLabel>{{ 'Data Drive' | i18n }}</label>
|
||||
@if (mobile) {
|
||||
<select
|
||||
tuiSelect
|
||||
[(ngModel)]="selectedDataDrive"
|
||||
(ngModelChange)="onDataDriveChange($event)"
|
||||
[items]="drives"
|
||||
></select>
|
||||
} @else {
|
||||
<input
|
||||
tuiSelect
|
||||
[(ngModel)]="selectedDataDrive"
|
||||
(ngModelChange)="onDataDriveChange($event)"
|
||||
/>
|
||||
}
|
||||
@if (!mobile) {
|
||||
<tui-data-list-wrapper
|
||||
new
|
||||
*tuiTextfieldDropdown
|
||||
[items]="drives"
|
||||
[itemContent]="driveContent"
|
||||
/>
|
||||
}
|
||||
@if (preserveData === true) {
|
||||
<tui-icon
|
||||
icon="@tui.database"
|
||||
style="color: var(--tui-status-positive); pointer-events: none"
|
||||
/>
|
||||
}
|
||||
@if (preserveData === false) {
|
||||
<tui-icon
|
||||
icon="@tui.database-zap"
|
||||
style="color: var(--tui-status-negative); pointer-events: none"
|
||||
/>
|
||||
}
|
||||
<tui-icon [tuiTooltip]="dataDriveTooltip" />
|
||||
</tui-textfield>
|
||||
|
||||
<ng-template #driveContent let-drive>
|
||||
<div class="drive-item">
|
||||
<span class="drive-name">
|
||||
{{ drive.vendor || ('Unknown' | i18n) }}
|
||||
{{ drive.model || ('Drive' | i18n) }}
|
||||
</span>
|
||||
<small>
|
||||
{{ formatCapacity(drive.capacity) }} · {{ drive.logicalname }}
|
||||
</small>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<footer>
|
||||
@if (drives.length === 0) {
|
||||
<button tuiButton appearance="secondary" (click)="refresh()">
|
||||
{{ 'Refresh' | i18n }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!selectedOsDrive || !selectedDataDrive"
|
||||
(click)="continue()"
|
||||
>
|
||||
{{ 'Continue' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</footer>
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
@@ -209,10 +198,6 @@ export default class DrivesPage {
|
||||
'The drive where your StartOS data (services, settings, etc.) will be stored. This can be the same as the OS drive or a separate drive.',
|
||||
)
|
||||
|
||||
private readonly MIN_OS = 18 * 2 ** 30 // 18 GiB
|
||||
private readonly MIN_DATA = 20 * 2 ** 30 // 20 GiB
|
||||
private readonly MIN_BOTH = 38 * 2 ** 30 // 38 GiB
|
||||
|
||||
drives: DiskInfo[] = []
|
||||
loading = true
|
||||
shuttingDown = false
|
||||
@@ -221,17 +206,10 @@ export default class DrivesPage {
|
||||
selectedDataDrive: DiskInfo | null = null
|
||||
preserveData: boolean | null = null
|
||||
|
||||
readonly osDisabled = (drive: DiskInfo): boolean =>
|
||||
drive.capacity < this.MIN_OS
|
||||
|
||||
dataDisabled = (drive: DiskInfo): boolean => drive.capacity < this.MIN_DATA
|
||||
|
||||
readonly driveName = (drive: DiskInfo): string =>
|
||||
[drive.vendor, drive.model].filter(Boolean).join(' ') ||
|
||||
this.i18n.transform('Unknown Drive')
|
||||
|
||||
readonly stringify = (drive: DiskInfo | null) =>
|
||||
drive ? this.driveName(drive) : ''
|
||||
drive
|
||||
? `${drive.vendor || this.i18n.transform('Unknown')} ${drive.model || this.i18n.transform('Drive')}`
|
||||
: ''
|
||||
|
||||
formatCapacity(bytes: number): string {
|
||||
const gb = bytes / 1e9
|
||||
@@ -253,22 +231,6 @@ export default class DrivesPage {
|
||||
await this.loadDrives()
|
||||
}
|
||||
|
||||
onOsDriveChange(osDrive: DiskInfo | null) {
|
||||
this.selectedOsDrive = osDrive
|
||||
this.dataDisabled = (drive: DiskInfo) => {
|
||||
if (osDrive && drive.logicalname === osDrive.logicalname) {
|
||||
return drive.capacity < this.MIN_BOTH
|
||||
}
|
||||
return drive.capacity < this.MIN_DATA
|
||||
}
|
||||
|
||||
// Clear data drive if it's now invalid
|
||||
if (this.selectedDataDrive && this.dataDisabled(this.selectedDataDrive)) {
|
||||
this.selectedDataDrive = null
|
||||
this.preserveData = null
|
||||
}
|
||||
}
|
||||
|
||||
onDataDriveChange(drive: DiskInfo | null) {
|
||||
this.preserveData = null
|
||||
|
||||
@@ -438,7 +400,7 @@ export default class DrivesPage {
|
||||
|
||||
private async loadDrives() {
|
||||
try {
|
||||
this.drives = (await this.api.getDisks()).filter(d => d.capacity > 0)
|
||||
this.drives = await this.api.getDisks()
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, inject, signal } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import {
|
||||
getAllKeyboardsSorted,
|
||||
@@ -71,6 +72,7 @@ import { StateService } from '../services/state.service'
|
||||
],
|
||||
})
|
||||
export default class KeyboardPage {
|
||||
private readonly router = inject(Router)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly stateService = inject(StateService)
|
||||
|
||||
@@ -101,9 +103,22 @@ export default class KeyboardPage {
|
||||
})
|
||||
|
||||
this.stateService.keyboard = this.selected.layout
|
||||
await this.stateService.navigateAfterLocale()
|
||||
await this.navigateToNextStep()
|
||||
} finally {
|
||||
this.saving.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
private async navigateToNextStep() {
|
||||
if (this.stateService.dataDriveGuid) {
|
||||
if (this.stateService.attach) {
|
||||
this.stateService.setupType = 'attach'
|
||||
await this.router.navigate(['/password'])
|
||||
} else {
|
||||
await this.router.navigate(['/home'])
|
||||
}
|
||||
} else {
|
||||
await this.router.navigate(['/drives'])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,12 +141,8 @@ export default class LanguagePage {
|
||||
|
||||
try {
|
||||
await this.api.setLanguage({ language: this.selected.name })
|
||||
|
||||
if (this.stateService.kiosk) {
|
||||
await this.router.navigate(['/keyboard'])
|
||||
} else {
|
||||
await this.stateService.navigateAfterLocale()
|
||||
}
|
||||
// Always go to keyboard selection
|
||||
await this.router.navigate(['/keyboard'])
|
||||
} finally {
|
||||
this.saving.set(false)
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ export default class SuccessPage implements AfterViewInit {
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
await this.api.echo({ message: 'ping' }, `${this.lanAddress}/rpc/v1`)
|
||||
await this.api.echo({ message: 'ping' }, this.lanAddress)
|
||||
return
|
||||
} catch {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
|
||||
@@ -191,118 +191,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
const GiB = 2 ** 30
|
||||
|
||||
const MOCK_DISKS: DiskInfo[] = [
|
||||
// 0 capacity - should be hidden entirely
|
||||
{
|
||||
logicalname: '/dev/sdd',
|
||||
vendor: 'Generic',
|
||||
model: 'Card Reader',
|
||||
partitions: [],
|
||||
capacity: 0,
|
||||
guid: null,
|
||||
},
|
||||
// 10 GiB - too small for OS and data; also tests both vendor+model null
|
||||
{
|
||||
logicalname: '/dev/sde',
|
||||
vendor: null,
|
||||
model: null,
|
||||
partitions: [
|
||||
{
|
||||
logicalname: '/dev/sde1',
|
||||
label: null,
|
||||
capacity: 10 * GiB,
|
||||
used: null,
|
||||
startOs: {},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 10 * GiB,
|
||||
guid: null,
|
||||
},
|
||||
// 18 GiB - exact OS boundary; tests vendor null with model present
|
||||
{
|
||||
logicalname: '/dev/sdf',
|
||||
vendor: null,
|
||||
model: 'SATA Flash Drive',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: '/dev/sdf1',
|
||||
label: null,
|
||||
capacity: 18 * GiB,
|
||||
used: null,
|
||||
startOs: {},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 18 * GiB,
|
||||
guid: null,
|
||||
},
|
||||
// 20 GiB - exact data boundary; tests vendor present with model null
|
||||
{
|
||||
logicalname: '/dev/sdg',
|
||||
vendor: 'PNY',
|
||||
model: null,
|
||||
partitions: [
|
||||
{
|
||||
logicalname: '/dev/sdg1',
|
||||
label: null,
|
||||
capacity: 20 * GiB,
|
||||
used: null,
|
||||
startOs: {},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 20 * GiB,
|
||||
guid: null,
|
||||
},
|
||||
// 30 GiB - OK for OS or data alone, too small for both (< 38 GiB)
|
||||
{
|
||||
logicalname: '/dev/sdh',
|
||||
vendor: 'SanDisk',
|
||||
model: 'Ultra',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: '/dev/sdh1',
|
||||
label: null,
|
||||
capacity: 30 * GiB,
|
||||
used: null,
|
||||
startOs: {},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 30 * GiB,
|
||||
guid: null,
|
||||
},
|
||||
// 30 GiB with existing StartOS data - tests preserve/overwrite + capacity constraint
|
||||
{
|
||||
logicalname: '/dev/sdi',
|
||||
vendor: 'Kingston',
|
||||
model: 'A400',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: '/dev/sdi1',
|
||||
label: null,
|
||||
capacity: 30 * GiB,
|
||||
used: null,
|
||||
startOs: {
|
||||
'small-server-id': {
|
||||
hostname: 'small-server',
|
||||
version: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: 'small-existing-guid',
|
||||
},
|
||||
],
|
||||
capacity: 30 * GiB,
|
||||
guid: 'small-existing-guid',
|
||||
},
|
||||
// 500 GB - large, always OK
|
||||
{
|
||||
logicalname: '/dev/sda',
|
||||
vendor: 'Samsung',
|
||||
@@ -320,7 +209,6 @@ const MOCK_DISKS: DiskInfo[] = [
|
||||
capacity: 500000000000,
|
||||
guid: null,
|
||||
},
|
||||
// 1 TB with existing StartOS data
|
||||
{
|
||||
logicalname: '/dev/sdb',
|
||||
vendor: 'Crucial',
|
||||
@@ -347,7 +235,6 @@ const MOCK_DISKS: DiskInfo[] = [
|
||||
capacity: 1000000000000,
|
||||
guid: 'existing-guid',
|
||||
},
|
||||
// 2 TB
|
||||
{
|
||||
logicalname: '/dev/sdc',
|
||||
vendor: 'WD',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ApiService } from './api.service'
|
||||
|
||||
@@ -30,7 +29,6 @@ export type RecoverySource =
|
||||
})
|
||||
export class StateService {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly router = inject(Router)
|
||||
|
||||
// Determined at app init
|
||||
kiosk = false
|
||||
@@ -47,23 +45,6 @@ export class StateService {
|
||||
setupType?: SetupType
|
||||
recoverySource?: RecoverySource
|
||||
|
||||
/**
|
||||
* Navigate to the appropriate step after language/keyboard selection.
|
||||
* Keyboard selection is only needed in kiosk mode.
|
||||
*/
|
||||
async navigateAfterLocale(): Promise<void> {
|
||||
if (this.dataDriveGuid) {
|
||||
if (this.attach) {
|
||||
this.setupType = 'attach'
|
||||
await this.router.navigate(['/password'])
|
||||
} else {
|
||||
await this.router.navigate(['/home'])
|
||||
}
|
||||
} else {
|
||||
await this.router.navigate(['/drives'])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for attach flow (existing data drive)
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { enableProdMode } from '@angular/core'
|
||||
import { bootstrapApplication } from '@angular/platform-browser'
|
||||
import { AppComponent } from 'src/app/app.component'
|
||||
import { APP_CONFIG } from 'src/app/app.config'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
|
||||
import { AppModule } from './app/app.module'
|
||||
import { environment } from './environments/environment'
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode()
|
||||
}
|
||||
|
||||
bootstrapApplication(AppComponent, APP_CONFIG).catch(console.error)
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err))
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M22.7 17.21h-3.83v-1.975c0-1.572-1.3-2.86-2.86-2.86s-2.86 1.3-2.86 2.86v1.975H9.33v-1.975c0-3.708 3.023-6.7 6.7-6.7 3.708 0 6.7 3.023 6.7 6.7z" fill="#ffa400"/><path d="M24.282 17.21H7.758a1.27 1.27 0 0 0-1.29 1.29V30.7A1.27 1.27 0 0 0 7.758 32h16.524a1.27 1.27 0 0 0 1.29-1.29V18.5c-.04-.725-.605-1.3-1.3-1.3zm-7.456 8.02v1.652c0 .443-.363.846-.846.846-.443 0-.846-.363-.846-.846V25.23c-.524-.282-.846-.846-.846-1.49 0-.927.766-1.693 1.693-1.693s1.693.766 1.693 1.693c.04.645-.322 1.21-.846 1.49z" fill="#003a70"/><path d="M6.066 15.395h-4a1.17 1.17 0 0 1-1.169-1.169 1.17 1.17 0 0 1 1.169-1.169h4a1.17 1.17 0 0 1 1.169 1.169 1.17 1.17 0 0 1-1.169 1.169zm2.82-6.287a1.03 1.03 0 0 1-.725-.282l-3.144-2.58c-.484-.403-.564-1.128-.16-1.652.403-.484 1.128-.564 1.652-.16l3.144 2.58c.484.403.564 1.128.16 1.652-.282.282-.605.443-.927.443zm7.134-2.74a1.17 1.17 0 0 1-1.169-1.169V1.17A1.17 1.17 0 0 1 16.02 0a1.17 1.17 0 0 1 1.169 1.169V5.2a1.17 1.17 0 0 1-1.169 1.169zm7.093 2.74c-.322 0-.685-.16-.887-.443-.403-.484-.322-1.25.16-1.652l3.144-2.58c.484-.403 1.25-.322 1.652.16s.322 1.25-.16 1.652l-3.144 2.58a1.13 1.13 0 0 1-.766.282zm6.81 6.287h-4.03a1.17 1.17 0 0 1-1.169-1.169 1.17 1.17 0 0 1 1.169-1.169h4.03a1.17 1.17 0 0 1 1.169 1.169 1.17 1.17 0 0 1-1.169 1.169z" fill="#ffa400"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -16,7 +16,7 @@ export const VERSION = new InjectionToken<string>('VERSION')
|
||||
host: {
|
||||
target: '_blank',
|
||||
rel: 'noreferrer',
|
||||
'[attr.href]': 'url()',
|
||||
'[href]': 'url()',
|
||||
},
|
||||
})
|
||||
export class DocsLinkDirective {
|
||||
|
||||
@@ -360,6 +360,7 @@ export default {
|
||||
377: 'StartOS-Sicherungen erkannt',
|
||||
378: 'Keine StartOS-Sicherungen erkannt',
|
||||
379: 'StartOS-Version',
|
||||
381: 'SMTP-Zugangsdaten',
|
||||
382: 'Test-E-Mail senden',
|
||||
383: 'Senden',
|
||||
384: 'E-Mail wird gesendet',
|
||||
@@ -643,6 +644,7 @@ export default {
|
||||
706: 'Beibehalten',
|
||||
707: 'Überschreiben',
|
||||
708: 'Entsperren',
|
||||
709: 'Laufwerk',
|
||||
710: 'Übertragen',
|
||||
711: 'Die Liste ist leer',
|
||||
712: 'Jetzt neu starten',
|
||||
@@ -657,6 +659,8 @@ export default {
|
||||
721: 'Gateway für ausgehenden Datenverkehr auswählen',
|
||||
722: 'Der Typ des Gateways',
|
||||
723: 'Nur ausgehend',
|
||||
724: 'Als Standard für ausgehenden Verkehr festlegen',
|
||||
725: 'Gesamten ausgehenden Datenverkehr über dieses Gateway leiten',
|
||||
726: 'WireGuard-Konfigurationsdatei',
|
||||
727: 'Eingehend/Ausgehend',
|
||||
728: 'StartTunnel (Eingehend/Ausgehend)',
|
||||
@@ -665,6 +669,7 @@ export default {
|
||||
731: 'Öffentliche Domain',
|
||||
732: 'Private Domain',
|
||||
733: 'Ausblenden',
|
||||
734: 'Standard ausgehend',
|
||||
735: 'Zertifikat',
|
||||
736: 'Selbstsigniert',
|
||||
737: 'Portweiterleitung',
|
||||
@@ -699,14 +704,4 @@ export default {
|
||||
774: 'Der Portstatus kann nicht ermittelt werden, solange der Dienst nicht läuft',
|
||||
775: 'Diese Adresse funktioniert nicht aus Ihrem lokalen Netzwerk aufgrund einer Router-Hairpinning-Einschränkung',
|
||||
776: 'Aktion nicht gefunden',
|
||||
777: 'Diese Domain wird auch gelten für',
|
||||
778: 'Plugin',
|
||||
779: 'Öffentlich',
|
||||
780: 'Privat',
|
||||
781: 'Lokal',
|
||||
782: 'Unbekanntes Laufwerk',
|
||||
783: 'Muss eine gültige E-Mail-Adresse sein',
|
||||
786: 'Automatisch',
|
||||
787: 'Ausgehender Datenverkehr',
|
||||
788: 'Gateway verwenden',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -359,6 +359,7 @@ export const ENGLISH: Record<string, number> = {
|
||||
'StartOS backups detected': 377,
|
||||
'No StartOS backups detected': 378,
|
||||
'StartOS Version': 379,
|
||||
'SMTP Credentials': 381,
|
||||
'Send test email': 382,
|
||||
'Send': 383,
|
||||
'Sending email': 384,
|
||||
@@ -643,6 +644,7 @@ export const ENGLISH: Record<string, number> = {
|
||||
'Preserve': 706,
|
||||
'Overwrite': 707,
|
||||
'Unlock': 708,
|
||||
'Drive': 709, // the noun, a storage device
|
||||
'Transfer': 710, // the verb
|
||||
'The list is empty': 711,
|
||||
'Restart now': 712,
|
||||
@@ -657,6 +659,8 @@ export const ENGLISH: Record<string, number> = {
|
||||
'Select the gateway for outbound traffic': 721,
|
||||
'The type of gateway': 722,
|
||||
'Outbound Only': 723,
|
||||
'Set as default outbound': 724,
|
||||
'Route all outbound traffic through this gateway': 725,
|
||||
'WireGuard Config File': 726,
|
||||
'Inbound/Outbound': 727,
|
||||
'StartTunnel (Inbound/Outbound)': 728,
|
||||
@@ -665,6 +669,7 @@ export const ENGLISH: Record<string, number> = {
|
||||
'Public Domain': 731,
|
||||
'Private Domain': 732,
|
||||
'Hide': 733,
|
||||
'default outbound': 734,
|
||||
'Certificate': 735,
|
||||
'Self signed': 736,
|
||||
'Port Forwarding': 737,
|
||||
@@ -699,14 +704,4 @@ export const ENGLISH: Record<string, number> = {
|
||||
'Port status cannot be determined while service is not running': 774,
|
||||
'This address will not work from your local network due to a router hairpinning limitation': 775,
|
||||
'Action not found': 776,
|
||||
'This domain will also apply to': 777,
|
||||
'Plugin': 778,
|
||||
'Public': 779, // as in, publicly accessible
|
||||
'Private': 780, // as in, privately accessible
|
||||
'Local': 781, // as in, locally accessible
|
||||
'Unknown Drive': 782,
|
||||
'Must be a valid email address': 783,
|
||||
'Auto': 786,
|
||||
'Outbound Traffic': 787,
|
||||
'Use gateway': 788,
|
||||
}
|
||||
|
||||
@@ -360,6 +360,7 @@ export default {
|
||||
377: 'Copias de seguridad de StartOS detectadas',
|
||||
378: 'No se detectaron copias de seguridad de StartOS',
|
||||
379: 'Versión de StartOS',
|
||||
381: 'Credenciales SMTP',
|
||||
382: 'Enviar correo de prueba',
|
||||
383: 'Enviar',
|
||||
384: 'Enviando correo',
|
||||
@@ -643,6 +644,7 @@ export default {
|
||||
706: 'Conservar',
|
||||
707: 'Sobrescribir',
|
||||
708: 'Desbloquear',
|
||||
709: 'Unidad',
|
||||
710: 'Transferir',
|
||||
711: 'La lista está vacía',
|
||||
712: 'Reiniciar ahora',
|
||||
@@ -657,6 +659,8 @@ export default {
|
||||
721: 'Selecciona la puerta de enlace para el tráfico saliente',
|
||||
722: 'El tipo de puerta de enlace',
|
||||
723: 'Solo saliente',
|
||||
724: 'Establecer como saliente predeterminado',
|
||||
725: 'Enrutar todo el tráfico saliente a través de esta puerta de enlace',
|
||||
726: 'Archivo de configuración WireGuard',
|
||||
727: 'Entrante/Saliente',
|
||||
728: 'StartTunnel (Entrante/Saliente)',
|
||||
@@ -665,6 +669,7 @@ export default {
|
||||
731: 'Dominio público',
|
||||
732: 'Dominio privado',
|
||||
733: 'Ocultar',
|
||||
734: 'saliente predeterminado',
|
||||
735: 'Certificado',
|
||||
736: 'Autofirmado',
|
||||
737: 'Reenvío de puertos',
|
||||
@@ -699,14 +704,4 @@ export default {
|
||||
774: 'El estado del puerto no se puede determinar mientras el servicio no está en ejecución',
|
||||
775: 'Esta dirección no funcionará desde tu red local debido a una limitación de hairpinning del router',
|
||||
776: 'Acción no encontrada',
|
||||
777: 'Este dominio también se aplicará a',
|
||||
778: 'Plugin',
|
||||
779: 'Público',
|
||||
780: 'Privado',
|
||||
781: 'Local',
|
||||
782: 'Unidad desconocida',
|
||||
783: 'Debe ser una dirección de correo electrónico válida',
|
||||
786: 'Automático',
|
||||
787: 'Tráfico saliente',
|
||||
788: 'Usar gateway',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -360,6 +360,7 @@ export default {
|
||||
377: 'Sauvegardes StartOS détectées',
|
||||
378: 'Aucune sauvegarde StartOS détectée',
|
||||
379: 'Version de StartOS',
|
||||
381: 'Identifiants SMTP',
|
||||
382: 'Envoyer un email de test',
|
||||
383: 'Envoyer',
|
||||
384: 'Envoi de l’email',
|
||||
@@ -643,6 +644,7 @@ export default {
|
||||
706: 'Conserver',
|
||||
707: 'Écraser',
|
||||
708: 'Déverrouiller',
|
||||
709: 'Disque',
|
||||
710: 'Transférer',
|
||||
711: 'La liste est vide',
|
||||
712: 'Redémarrer maintenant',
|
||||
@@ -657,6 +659,8 @@ export default {
|
||||
721: 'Sélectionnez la passerelle pour le trafic sortant',
|
||||
722: 'Le type de passerelle',
|
||||
723: 'Sortant uniquement',
|
||||
724: 'Définir comme sortant par défaut',
|
||||
725: 'Acheminer tout le trafic sortant via cette passerelle',
|
||||
726: 'Fichier de configuration WireGuard',
|
||||
727: 'Entrant/Sortant',
|
||||
728: 'StartTunnel (Entrant/Sortant)',
|
||||
@@ -665,6 +669,7 @@ export default {
|
||||
731: 'Domaine public',
|
||||
732: 'Domaine privé',
|
||||
733: 'Masquer',
|
||||
734: 'sortant par défaut',
|
||||
735: 'Certificat',
|
||||
736: 'Auto-signé',
|
||||
737: 'Redirection de ports',
|
||||
@@ -699,14 +704,4 @@ export default {
|
||||
774: "L'état du port ne peut pas être déterminé tant que le service n'est pas en cours d'exécution",
|
||||
775: "Cette adresse ne fonctionnera pas depuis votre réseau local en raison d'une limitation de hairpinning du routeur",
|
||||
776: 'Action introuvable',
|
||||
777: "Ce domaine s'appliquera également à",
|
||||
778: 'Plugin',
|
||||
779: 'Public',
|
||||
780: 'Privé',
|
||||
781: 'Local',
|
||||
782: 'Lecteur inconnu',
|
||||
783: 'Doit être une adresse e-mail valide',
|
||||
786: 'Automatique',
|
||||
787: 'Trafic sortant',
|
||||
788: 'Utiliser la passerelle',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -360,6 +360,7 @@ export default {
|
||||
377: 'Wykryto kopie zapasowe StartOS',
|
||||
378: 'Nie wykryto kopii zapasowych StartOS',
|
||||
379: 'Wersja StartOS',
|
||||
381: 'Dane logowania SMTP',
|
||||
382: 'Wyślij e-mail testowy',
|
||||
383: 'Wyślij',
|
||||
384: 'Wysyłanie e-maila',
|
||||
@@ -643,6 +644,7 @@ export default {
|
||||
706: 'Zachowaj',
|
||||
707: 'Nadpisz',
|
||||
708: 'Odblokuj',
|
||||
709: 'Dysk',
|
||||
710: 'Przenieś',
|
||||
711: 'Lista jest pusta',
|
||||
712: 'Uruchom ponownie teraz',
|
||||
@@ -657,6 +659,8 @@ export default {
|
||||
721: 'Wybierz bramę dla ruchu wychodzącego',
|
||||
722: 'Typ bramy',
|
||||
723: 'Tylko wychodzący',
|
||||
724: 'Ustaw jako domyślne wychodzące',
|
||||
725: 'Kieruj cały ruch wychodzący przez tę bramę',
|
||||
726: 'Plik konfiguracyjny WireGuard',
|
||||
727: 'Przychodzący/Wychodzący',
|
||||
728: 'StartTunnel (Przychodzący/Wychodzący)',
|
||||
@@ -665,6 +669,7 @@ export default {
|
||||
731: 'Domena publiczna',
|
||||
732: 'Domena prywatna',
|
||||
733: 'Ukryj',
|
||||
734: 'domyślne wychodzące',
|
||||
735: 'Certyfikat',
|
||||
736: 'Samopodpisany',
|
||||
737: 'Przekierowanie portów',
|
||||
@@ -699,14 +704,4 @@ export default {
|
||||
774: 'Status portu nie może być określony, gdy usługa nie jest uruchomiona',
|
||||
775: 'Ten adres nie będzie działać z Twojej sieci lokalnej z powodu ograniczenia hairpinning routera',
|
||||
776: 'Nie znaleziono akcji',
|
||||
777: 'Ta domena będzie również dotyczyć',
|
||||
778: 'Wtyczka',
|
||||
779: 'Publiczny',
|
||||
780: 'Prywatny',
|
||||
781: 'Lokalny',
|
||||
782: 'Nieznany dysk',
|
||||
783: 'Musi być prawidłowy adres e-mail',
|
||||
786: 'Automatycznie',
|
||||
787: 'Ruch wychodzący',
|
||||
788: 'Użyj bramy',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
// converts bytes to gigabytes
|
||||
@Pipe({
|
||||
name: 'convertBytes',
|
||||
})
|
||||
export class ConvertBytesPipe implements PipeTransform {
|
||||
transform(bytes: number): string {
|
||||
return convertBytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
export function convertBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
8
web/projects/shared/src/pipes/exver/exver.module.ts
Normal file
8
web/projects/shared/src/pipes/exver/exver.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { ExverComparesPipe, ExverSatisfiesPipe } from './exver.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ExverComparesPipe, ExverSatisfiesPipe],
|
||||
exports: [ExverComparesPipe, ExverSatisfiesPipe],
|
||||
})
|
||||
export class ExverPipesModule {}
|
||||
@@ -1,11 +1,28 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { Exver } from '../services/exver.service'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { Exver } from '../../services/exver.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'satisfiesExver',
|
||||
standalone: false,
|
||||
})
|
||||
export class ExverSatisfiesPipe implements PipeTransform {
|
||||
constructor(private readonly exver: Exver) {}
|
||||
|
||||
transform(versionUnderTest?: string, range?: string): boolean {
|
||||
return (
|
||||
!!versionUnderTest &&
|
||||
!!range &&
|
||||
this.exver.satisfies(versionUnderTest, range)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'compareExver',
|
||||
standalone: false,
|
||||
})
|
||||
export class ExverComparesPipe implements PipeTransform {
|
||||
private readonly exver = inject(Exver)
|
||||
constructor(private readonly exver: Exver) {}
|
||||
|
||||
transform(first: string, second: string): SemverResult {
|
||||
try {
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { isEmptyObject } from '../util/misc.util'
|
||||
import { isEmptyObject } from '../../util/misc.util'
|
||||
|
||||
@Pipe({
|
||||
name: 'empty',
|
||||
standalone: false,
|
||||
})
|
||||
export class EmptyPipe implements PipeTransform {
|
||||
transform(val: object | [] = {}): boolean {
|
||||
return Array.isArray(val) ? !val.length : isEmptyObject(val)
|
||||
if (Array.isArray(val)) return !val.length
|
||||
return isEmptyObject(val)
|
||||
}
|
||||
}
|
||||
11
web/projects/shared/src/pipes/shared/includes.pipe.ts
Normal file
11
web/projects/shared/src/pipes/shared/includes.pipe.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
@Pipe({
|
||||
name: 'includes',
|
||||
standalone: false,
|
||||
})
|
||||
export class IncludesPipe implements PipeTransform {
|
||||
transform<T>(list: T[], val: T): boolean {
|
||||
return list.includes(val)
|
||||
}
|
||||
}
|
||||
10
web/projects/shared/src/pipes/shared/shared.module.ts
Normal file
10
web/projects/shared/src/pipes/shared/shared.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IncludesPipe } from './includes.pipe'
|
||||
import { EmptyPipe } from './empty.pipe'
|
||||
import { TrustUrlPipe } from './trust.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [IncludesPipe, EmptyPipe, TrustUrlPipe],
|
||||
exports: [IncludesPipe, EmptyPipe, TrustUrlPipe],
|
||||
})
|
||||
export class SharedPipesModule {}
|
||||
35
web/projects/shared/src/pipes/shared/sort.pipe.ts
Normal file
35
web/projects/shared/src/pipes/shared/sort.pipe.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
@Pipe({
|
||||
name: 'sort',
|
||||
standalone: false,
|
||||
})
|
||||
export class SortPipe implements PipeTransform {
|
||||
transform(
|
||||
value: any[],
|
||||
column: string = '',
|
||||
direction: string = 'asc',
|
||||
): any[] {
|
||||
// If the value is not an array or is empty, return the original value
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
return value
|
||||
}
|
||||
|
||||
// Clone the array to avoid modifying the original value
|
||||
const sortedValue = [...value]
|
||||
|
||||
// Define the sorting function based on the column and direction parameters
|
||||
const sortingFn = (a: any, b: any): number => {
|
||||
if (a[column] < b[column]) {
|
||||
return direction === 'asc' ? -1 : 1
|
||||
} else if (a[column] > b[column]) {
|
||||
return direction === 'asc' ? 1 : -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the array and return the result
|
||||
return sortedValue.sort(sortingFn)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
|
||||
|
||||
@Pipe({
|
||||
name: 'trustUrl',
|
||||
standalone: false,
|
||||
})
|
||||
export class TrustUrlPipe implements PipeTransform {
|
||||
private readonly sanitizer = inject(DomSanitizer)
|
||||
constructor(private readonly sanitizer: DomSanitizer) {}
|
||||
|
||||
transform(base64Icon: string): SafeResourceUrl {
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(base64Icon)
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { ConvertBytesPipe, DurationToSecondsPipe } from './unit-conversion.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ConvertBytesPipe, DurationToSecondsPipe],
|
||||
exports: [ConvertBytesPipe, DurationToSecondsPipe],
|
||||
})
|
||||
export class UnitConversionPipesModule {}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
// converts bytes to gigabytes
|
||||
@Pipe({
|
||||
name: 'convertBytes',
|
||||
standalone: false,
|
||||
})
|
||||
export class ConvertBytesPipe implements PipeTransform {
|
||||
transform(bytes: number): string {
|
||||
return convertBytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
export function convertBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'durationToSeconds',
|
||||
standalone: false,
|
||||
})
|
||||
export class DurationToSecondsPipe implements PipeTransform {
|
||||
transform(duration?: string | null): number {
|
||||
if (!duration) return 0
|
||||
|
||||
const regex = /^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/
|
||||
const [, num, , unit] = duration.match(regex) || []
|
||||
const multiplier = (unit && unitsToSeconds[unit]) || NaN
|
||||
|
||||
return unit ? Number(num) * multiplier : NaN
|
||||
}
|
||||
}
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
|
||||
const unitsToSeconds: Record<string, number> = {
|
||||
ns: 1e-9,
|
||||
µs: 1e-6,
|
||||
ms: 0.001,
|
||||
s: 1,
|
||||
m: 60,
|
||||
h: 3600,
|
||||
d: 86400,
|
||||
}
|
||||
@@ -20,10 +20,14 @@ export * from './i18n/i18n.providers'
|
||||
export * from './i18n/i18n.service'
|
||||
export * from './i18n/localize.pipe'
|
||||
|
||||
export * from './pipes/exver-compares.pipe'
|
||||
export * from './pipes/empty.pipe'
|
||||
export * from './pipes/trust.pipe'
|
||||
export * from './pipes/convert-bytes.pipe'
|
||||
export * from './pipes/exver/exver.module'
|
||||
export * from './pipes/exver/exver.pipe'
|
||||
export * from './pipes/shared/shared.module'
|
||||
export * from './pipes/shared/empty.pipe'
|
||||
export * from './pipes/shared/includes.pipe'
|
||||
export * from './pipes/shared/trust.pipe'
|
||||
export * from './pipes/unit-conversion/unit-conversion.module'
|
||||
export * from './pipes/unit-conversion/unit-conversion.pipe'
|
||||
export * from './pipes/markdown.pipe'
|
||||
|
||||
export * from './services/copy.service'
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router'
|
||||
import { Router, RouterLink, RouterLinkActive } from '@angular/router'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiBadgeNotification } from '@taiga-ui/kit'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { SidebarService } from 'src/app/services/sidebar.service'
|
||||
import { UpdateService } from 'src/app/services/update.service'
|
||||
|
||||
@@ -35,6 +38,15 @@ import { UpdateService } from 'src/app/services/update.service'
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.log-out"
|
||||
appearance="neutral"
|
||||
size="s"
|
||||
(click)="logout()"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
@@ -67,6 +79,12 @@ import { UpdateService } from 'src/app/services/update.service'
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
@@ -88,7 +106,12 @@ import { UpdateService } from 'src/app/services/update.service'
|
||||
},
|
||||
})
|
||||
export class Nav {
|
||||
private readonly service = inject(AuthService)
|
||||
private readonly router = inject(Router)
|
||||
protected readonly sidebars = inject(SidebarService)
|
||||
protected readonly api = inject(ApiService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
protected readonly update = inject(UpdateService)
|
||||
|
||||
protected readonly routes = [
|
||||
@@ -108,4 +131,18 @@ export class Nav {
|
||||
link: 'port-forwards',
|
||||
},
|
||||
] as const
|
||||
|
||||
protected async logout() {
|
||||
const loader = this.loader.open().subscribe()
|
||||
try {
|
||||
await this.api.logout()
|
||||
this.service.authenticated.set(false)
|
||||
this.router.navigate(['.'])
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,6 @@ import { MappedDevice, PortForwardsData } from './utils'
|
||||
@Component({
|
||||
template: `
|
||||
<form tuiForm [formGroup]="form">
|
||||
<tui-textfield>
|
||||
<label tuiLabel>Label</label>
|
||||
<input tuiTextfield formControlName="label" />
|
||||
</tui-textfield>
|
||||
<tui-error formControlName="label" [error]="[] | tuiFieldError | async" />
|
||||
<tui-textfield tuiChevron>
|
||||
<label tuiLabel>External IP</label>
|
||||
@if (mobile) {
|
||||
@@ -166,7 +161,6 @@ export class PortForwardsAdd {
|
||||
injectContext<TuiDialogContext<void, PortForwardsData>>()
|
||||
|
||||
protected readonly form = inject(NonNullableFormBuilder).group({
|
||||
label: ['', Validators.required],
|
||||
externalip: ['', Validators.required],
|
||||
externalport: [null as number | null, Validators.required],
|
||||
device: [null as MappedDevice | null, Validators.required],
|
||||
@@ -191,21 +185,19 @@ export class PortForwardsAdd {
|
||||
|
||||
const loader = this.loading.open().subscribe()
|
||||
|
||||
const { label, externalip, externalport, device, internalport, also80 } =
|
||||
const { externalip, externalport, device, internalport, also80 } =
|
||||
this.form.getRawValue()
|
||||
|
||||
try {
|
||||
await this.api.addForward({
|
||||
source: `${externalip}:${externalport}`,
|
||||
target: `${device!.ip}:${internalport}`,
|
||||
label,
|
||||
})
|
||||
|
||||
if (externalport === 443 && internalport === 443 && also80) {
|
||||
await this.api.addForward({
|
||||
source: `${externalip}:80`,
|
||||
target: `${device!.ip}:443`,
|
||||
label: `${label} (HTTP redirect)`,
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import {
|
||||
NonNullableFormBuilder,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDialogContext,
|
||||
TuiError,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiFieldErrorPipe } from '@taiga-ui/kit'
|
||||
import { TuiForm } from '@taiga-ui/layout'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
|
||||
export interface EditLabelData {
|
||||
readonly source: string
|
||||
readonly label: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<form tuiForm [formGroup]="form">
|
||||
<tui-textfield>
|
||||
<label tuiLabel>Label</label>
|
||||
<input tuiTextfield formControlName="label" />
|
||||
</tui-textfield>
|
||||
<tui-error formControlName="label" [error]="[] | tuiFieldError | async" />
|
||||
<footer>
|
||||
<button tuiButton [disabled]="form.invalid" (click)="onSave()">
|
||||
Save
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
TuiButton,
|
||||
TuiError,
|
||||
TuiFieldErrorPipe,
|
||||
TuiTextfield,
|
||||
TuiForm,
|
||||
],
|
||||
})
|
||||
export class PortForwardsEditLabel {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly loading = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
protected readonly context =
|
||||
injectContext<TuiDialogContext<void, EditLabelData>>()
|
||||
|
||||
protected readonly form = inject(NonNullableFormBuilder).group({
|
||||
label: [this.context.data.label, Validators.required],
|
||||
})
|
||||
|
||||
protected async onSave() {
|
||||
const loader = this.loading.open().subscribe()
|
||||
|
||||
try {
|
||||
await this.api.updateForwardLabel({
|
||||
source: this.context.data.source,
|
||||
label: this.form.getRawValue().label,
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const PORT_FORWARDS_EDIT_LABEL = new PolymorpheusComponent(
|
||||
PortForwardsEditLabel,
|
||||
)
|
||||
@@ -3,26 +3,18 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
Signal,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { utils } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiLoader,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiDialogService } from '@taiga-ui/experimental'
|
||||
import { TUI_CONFIRM, TuiSwitch } from '@taiga-ui/kit'
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { PORT_FORWARDS_ADD } from 'src/app/routes/home/routes/port-forwards/add'
|
||||
import { PORT_FORWARDS_EDIT_LABEL } from 'src/app/routes/home/routes/port-forwards/edit-label'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { TunnelData } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@@ -33,8 +25,6 @@ import { MappedDevice, MappedForward } from './utils'
|
||||
<table class="g-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Label</th>
|
||||
<th>External IP</th>
|
||||
<th>External Port</th>
|
||||
<th>Device</th>
|
||||
@@ -49,23 +39,6 @@ import { MappedDevice, MappedForward } from './utils'
|
||||
<tbody>
|
||||
@for (forward of forwards(); track $index) {
|
||||
<tr>
|
||||
<td>
|
||||
<tui-loader
|
||||
[showLoader]="toggling() === $index"
|
||||
size="xs"
|
||||
[overlay]="true"
|
||||
>
|
||||
<input
|
||||
tuiSwitch
|
||||
type="checkbox"
|
||||
size="s"
|
||||
[showIcons]="false"
|
||||
[ngModel]="forward.enabled"
|
||||
(ngModelChange)="onToggle(forward, $index)"
|
||||
/>
|
||||
</tui-loader>
|
||||
</td>
|
||||
<td>{{ forward.label || '—' }}</td>
|
||||
<td>{{ forward.externalip }}</td>
|
||||
<td>{{ forward.externalport }}</td>
|
||||
<td>{{ forward.device.name }}</td>
|
||||
@@ -74,30 +47,11 @@ import { MappedDevice, MappedForward } from './utils'
|
||||
<button
|
||||
tuiIconButton
|
||||
size="xs"
|
||||
tuiDropdown
|
||||
tuiDropdownOpen
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.ellipsis-vertical"
|
||||
iconStart="@tui.trash"
|
||||
(click)="onDelete(forward)"
|
||||
>
|
||||
Actions
|
||||
<tui-data-list *tuiTextfieldDropdown size="s">
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.pencil"
|
||||
new
|
||||
(click)="onEditLabel(forward)"
|
||||
>
|
||||
{{ forward.label ? 'Rename' : 'Add label' }}
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.trash"
|
||||
new
|
||||
(click)="onDelete(forward)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</tui-data-list>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -108,15 +62,7 @@ import { MappedDevice, MappedForward } from './utils'
|
||||
</table>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiButton,
|
||||
TuiDropdown,
|
||||
TuiDataList,
|
||||
TuiLoader,
|
||||
TuiSwitch,
|
||||
TuiTextfield,
|
||||
],
|
||||
imports: [ReactiveFormsModule, TuiButton],
|
||||
})
|
||||
export default class PortForwards {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
@@ -154,36 +100,19 @@ export default class PortForwards {
|
||||
)
|
||||
|
||||
protected readonly forwards = computed(() =>
|
||||
Object.entries(this.portForwards() || {}).map(([source, entry]) => {
|
||||
Object.entries(this.portForwards() || {}).map(([source, target]) => {
|
||||
const sourceSplit = source.split(':')
|
||||
const targetSplit = entry.target.split(':')
|
||||
const targetSplit = target.split(':')
|
||||
|
||||
return {
|
||||
externalip: sourceSplit[0]!,
|
||||
externalport: sourceSplit[1]!,
|
||||
device: this.devices().find(d => d.ip === targetSplit[0])!,
|
||||
internalport: targetSplit[1]!,
|
||||
label: entry.label,
|
||||
enabled: entry.enabled,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
protected readonly toggling = signal<number | null>(null)
|
||||
|
||||
protected async onToggle(forward: MappedForward, index: number) {
|
||||
this.toggling.set(index)
|
||||
const source = `${forward.externalip}:${forward.externalport}`
|
||||
|
||||
try {
|
||||
await this.api.setForwardEnabled({ source, enabled: !forward.enabled })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.toggling.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
protected onAdd(): void {
|
||||
this.dialogs
|
||||
.open(PORT_FORWARDS_ADD, {
|
||||
@@ -193,18 +122,6 @@ export default class PortForwards {
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
protected onEditLabel(forward: MappedForward): void {
|
||||
this.dialogs
|
||||
.open(PORT_FORWARDS_EDIT_LABEL, {
|
||||
label: 'Edit label',
|
||||
data: {
|
||||
source: `${forward.externalip}:${forward.externalport}`,
|
||||
label: forward.label,
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
protected onDelete({ externalip, externalport }: MappedForward): void {
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, { label: 'Are you sure?' })
|
||||
|
||||
@@ -10,8 +10,6 @@ export interface MappedForward {
|
||||
readonly externalport: string
|
||||
readonly device: MappedDevice
|
||||
readonly internalport: string
|
||||
readonly label: string
|
||||
readonly enabled: boolean
|
||||
}
|
||||
|
||||
export interface PortForwardsData {
|
||||
|
||||
@@ -4,14 +4,11 @@ import {
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { TuiAppearance, TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiDialogService } from '@taiga-ui/experimental'
|
||||
import { TuiBadge, TuiButtonLoading } from '@taiga-ui/kit'
|
||||
import { TuiCard, TuiCell } from '@taiga-ui/layout'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { UpdateService } from 'src/app/services/update.service'
|
||||
|
||||
import { CHANGE_PASSWORD } from './change-password'
|
||||
@@ -53,20 +50,6 @@ import { CHANGE_PASSWORD } from './change-password'
|
||||
</span>
|
||||
<button tuiButton size="s" (click)="onChangePassword()">Change</button>
|
||||
</div>
|
||||
<div tuiCell>
|
||||
<span tuiTitle>
|
||||
<strong>Logout</strong>
|
||||
</span>
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
appearance="secondary-destructive"
|
||||
iconStart="@tui.log-out"
|
||||
(click)="onLogout()"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -83,10 +66,6 @@ import { CHANGE_PASSWORD } from './change-password'
|
||||
export default class Settings {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly auth = inject(AuthService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly loading = inject(LoadingService)
|
||||
|
||||
protected readonly update = inject(UpdateService)
|
||||
protected readonly checking = signal(false)
|
||||
@@ -119,18 +98,4 @@ export default class Settings {
|
||||
this.applying.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
protected async onLogout() {
|
||||
const loader = this.loading.open().subscribe()
|
||||
|
||||
try {
|
||||
await this.api.logout()
|
||||
this.auth.authenticated.set(false)
|
||||
this.router.navigate(['/'])
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ export abstract class ApiService {
|
||||
// forwards
|
||||
abstract addForward(params: AddForwardReq): Promise<null> // port-forward.add
|
||||
abstract deleteForward(params: DeleteForwardReq): Promise<null> // port-forward.remove
|
||||
abstract updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> // port-forward.update-label
|
||||
abstract setForwardEnabled(params: SetForwardEnabledReq): Promise<null> // port-forward.set-enabled
|
||||
// update
|
||||
abstract checkUpdate(): Promise<TunnelUpdateResult> // update.check
|
||||
abstract applyUpdate(): Promise<TunnelUpdateResult> // update.apply
|
||||
@@ -62,23 +60,12 @@ export type DeleteDeviceReq = {
|
||||
export type AddForwardReq = {
|
||||
source: string // externalip:port
|
||||
target: string // internalip:port
|
||||
label: string
|
||||
}
|
||||
|
||||
export type DeleteForwardReq = {
|
||||
source: string
|
||||
}
|
||||
|
||||
export type UpdateForwardLabelReq = {
|
||||
source: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type SetForwardEnabledReq = {
|
||||
source: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type TunnelUpdateResult = {
|
||||
status: string
|
||||
installed: string
|
||||
|
||||
@@ -17,8 +17,6 @@ import {
|
||||
LoginReq,
|
||||
SubscribeRes,
|
||||
TunnelUpdateResult,
|
||||
SetForwardEnabledReq,
|
||||
UpdateForwardLabelReq,
|
||||
UpsertDeviceReq,
|
||||
UpsertSubnetReq,
|
||||
} from './api.service'
|
||||
@@ -106,14 +104,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'port-forward.remove', params })
|
||||
}
|
||||
|
||||
async updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> {
|
||||
return this.rpcRequest({ method: 'port-forward.update-label', params })
|
||||
}
|
||||
|
||||
async setForwardEnabled(params: SetForwardEnabledReq): Promise<null> {
|
||||
return this.rpcRequest({ method: 'port-forward.set-enabled', params })
|
||||
}
|
||||
|
||||
// update
|
||||
|
||||
async checkUpdate(): Promise<TunnelUpdateResult> {
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
LoginReq,
|
||||
SubscribeRes,
|
||||
TunnelUpdateResult,
|
||||
SetForwardEnabledReq,
|
||||
UpdateForwardLabelReq,
|
||||
UpsertDeviceReq,
|
||||
UpsertSubnetReq,
|
||||
} from './api.service'
|
||||
@@ -26,12 +24,7 @@ import {
|
||||
Revision,
|
||||
} from 'patch-db-client'
|
||||
import { toObservable } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
mockTunnelData,
|
||||
PortForwardEntry,
|
||||
WgClient,
|
||||
WgSubnet,
|
||||
} from '../patch-db/data-model'
|
||||
import { mockTunnelData, WgClient, WgSubnet } from '../patch-db/data-model'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -178,45 +171,11 @@ export class MockApiService extends ApiService {
|
||||
async addForward(params: AddForwardReq): Promise<null> {
|
||||
await pauseFor(1000)
|
||||
|
||||
const patch: AddOperation<PortForwardEntry>[] = [
|
||||
const patch: AddOperation<string>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/portForwards/${params.source}`,
|
||||
value: {
|
||||
target: params.target,
|
||||
label: params.label || '',
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> {
|
||||
await pauseFor(1000)
|
||||
|
||||
const patch: ReplaceOperation<string>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/portForwards/${params.source}/label`,
|
||||
value: params.label,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async setForwardEnabled(params: SetForwardEnabledReq): Promise<null> {
|
||||
await pauseFor(1000)
|
||||
|
||||
const patch: ReplaceOperation<boolean>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/portForwards/${params.source}/enabled`,
|
||||
value: params.enabled,
|
||||
value: params.target,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export type PortForwardEntry = {
|
||||
target: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type TunnelData = {
|
||||
wg: WgServer
|
||||
portForwards: Record<string, PortForwardEntry>
|
||||
portForwards: Record<string, string>
|
||||
gateways: Record<string, T.NetworkInterfaceInfo>
|
||||
}
|
||||
|
||||
@@ -45,12 +39,8 @@ export const mockTunnelData: TunnelData = {
|
||||
},
|
||||
},
|
||||
portForwards: {
|
||||
'69.1.1.42:443': { target: '10.59.0.2:443', label: 'HTTPS', enabled: true },
|
||||
'69.1.1.42:3000': {
|
||||
target: '10.59.0.2:3000',
|
||||
label: 'Grafana',
|
||||
enabled: true,
|
||||
},
|
||||
'69.1.1.42:443': '10.59.0.2:443',
|
||||
'69.1.1.42:3000': '10.59.0.2:3000',
|
||||
},
|
||||
gateways: {
|
||||
eth0: {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { i18nService } from '@start9labs/shared'
|
||||
import { TuiRoot } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { merge } from 'rxjs'
|
||||
import { ToastContainerComponent } from 'src/app/components/toast-container.component'
|
||||
import { PatchDataService } from './services/patch-data.service'
|
||||
import { DataModel } from './services/patch-db/data-model'
|
||||
import { PatchMonitorService } from './services/patch-monitor.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [TuiRoot, RouterOutlet, ToastContainerComponent],
|
||||
template: `
|
||||
<tui-root tuiTheme="dark">
|
||||
<router-outlet />
|
||||
@@ -30,6 +26,7 @@ import { PatchMonitorService } from './services/patch-monitor.service'
|
||||
font-family: 'Proxima Nova', system-ui;
|
||||
}
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent {
|
||||
private readonly i18n = inject(i18nService)
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import {
|
||||
provideHttpClient,
|
||||
withFetch,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import {
|
||||
ApplicationConfig,
|
||||
inject,
|
||||
provideAppInitializer,
|
||||
provideZoneChangeDetection,
|
||||
} from '@angular/core'
|
||||
import { UntypedFormBuilder } from '@angular/forms'
|
||||
import { provideAnimations } from '@angular/platform-browser/animations'
|
||||
import {
|
||||
ActivationStart,
|
||||
PreloadAllModules,
|
||||
provideRouter,
|
||||
Router,
|
||||
withComponentInputBinding,
|
||||
withDisabledInitialNavigation,
|
||||
withInMemoryScrolling,
|
||||
withPreloading,
|
||||
withRouterConfig,
|
||||
} from '@angular/router'
|
||||
import { provideServiceWorker } from '@angular/service-worker'
|
||||
import { WA_LOCATION } from '@ng-web-apis/common'
|
||||
import initArgon from '@start9labs/argon2'
|
||||
import {
|
||||
AbstractCategoryService,
|
||||
FilterPackagesPipe,
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
I18N_PROVIDERS,
|
||||
I18N_STORAGE,
|
||||
i18nService,
|
||||
Languages,
|
||||
RELATIVE_URL,
|
||||
VERSION,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
import { tuiObfuscateOptionsProvider } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TUI_DATE_FORMAT,
|
||||
TUI_DIALOGS_CLOSE,
|
||||
TUI_MEDIA,
|
||||
tuiAlertOptionsProvider,
|
||||
tuiButtonOptionsProvider,
|
||||
tuiDropdownOptionsProvider,
|
||||
tuiNumberFormatProvider,
|
||||
} from '@taiga-ui/core'
|
||||
import { provideEventPlugins } from '@taiga-ui/event-plugins'
|
||||
import {
|
||||
TUI_DATE_TIME_VALUE_TRANSFORMER,
|
||||
TUI_DATE_VALUE_TRANSFORMER,
|
||||
} from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, identity, merge, of, pairwise } from 'rxjs'
|
||||
import { FilterUpdatesPipe } from 'src/app/routes/portal/routes/updates/filter-updates.pipe'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { LiveApiService } from 'src/app/services/api/embassy-live-api.service'
|
||||
import { MockApiService } from 'src/app/services/api/embassy-mock-api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { CategoryService } from 'src/app/services/category.service'
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DateTransformerService } from 'src/app/services/date-transformer.service'
|
||||
import { DatetimeTransformerService } from 'src/app/services/datetime-transformer.service'
|
||||
import {
|
||||
PATCH_CACHE,
|
||||
PatchDbSource,
|
||||
} from 'src/app/services/patch-db/patch-db-source'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { StorageService } from 'src/app/services/storage.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
import { ROUTES } from './app.routes'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
ui: { api },
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
export const APP_CONFIG: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection(),
|
||||
provideAnimations(),
|
||||
provideEventPlugins(),
|
||||
provideHttpClient(withInterceptorsFromDi(), withFetch()),
|
||||
provideRouter(
|
||||
ROUTES,
|
||||
withDisabledInitialNavigation(),
|
||||
withComponentInputBinding(),
|
||||
withPreloading(PreloadAllModules),
|
||||
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }),
|
||||
withRouterConfig({ paramsInheritanceStrategy: 'always' }),
|
||||
),
|
||||
provideServiceWorker('ngsw-worker.js', {
|
||||
enabled: environment.useServiceWorker,
|
||||
// Register the ServiceWorker as soon as the application is stable
|
||||
// or after 30 seconds (whichever comes first).
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
I18N_PROVIDERS,
|
||||
FilterPackagesPipe,
|
||||
FilterUpdatesPipe,
|
||||
UntypedFormBuilder,
|
||||
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
tuiDropdownOptionsProvider({ appearance: 'start-os' }),
|
||||
tuiAlertOptionsProvider({
|
||||
autoClose: appearance => (appearance === 'negative' ? 0 : 3000),
|
||||
}),
|
||||
{
|
||||
provide: TUI_DATE_FORMAT,
|
||||
useValue: of({
|
||||
mode: 'MDY',
|
||||
separator: '/',
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: TUI_DATE_VALUE_TRANSFORMER,
|
||||
useClass: DateTransformerService,
|
||||
},
|
||||
{
|
||||
provide: TUI_DATE_TIME_VALUE_TRANSFORMER,
|
||||
useClass: DatetimeTransformerService,
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: PatchDB,
|
||||
deps: [PatchDbSource, PATCH_CACHE],
|
||||
useClass: PatchDB,
|
||||
},
|
||||
provideAppInitializer(() => {
|
||||
const i18n = inject(i18nService)
|
||||
const origin = inject(WA_LOCATION).origin
|
||||
const module_or_path = new URL('/assets/argon2_bg.wasm', origin)
|
||||
|
||||
initArgon({ module_or_path })
|
||||
inject(StorageService).migrate036()
|
||||
inject(AuthService).init()
|
||||
inject(ClientStorageService).init()
|
||||
inject(Router).initialNavigation()
|
||||
i18n.setLanguage(i18n.language || 'english')
|
||||
}),
|
||||
{
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
{
|
||||
provide: AbstractCategoryService,
|
||||
useClass: CategoryService,
|
||||
},
|
||||
{
|
||||
provide: TUI_DIALOGS_CLOSE,
|
||||
useFactory: () =>
|
||||
merge(
|
||||
inject(Router).events.pipe(filter(e => e instanceof ActivationStart)),
|
||||
inject(StateService).pipe(
|
||||
pairwise(),
|
||||
filter(
|
||||
([prev, curr]) =>
|
||||
prev === 'running' &&
|
||||
(curr === 'error' || curr === 'initializing'),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
provide: I18N_STORAGE,
|
||||
useFactory: () => {
|
||||
const api = inject(ApiService)
|
||||
|
||||
return (language: Languages) => api.setLanguage({ language })
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: VERSION,
|
||||
useFactory: () => inject(ConfigService).version,
|
||||
},
|
||||
tuiObfuscateOptionsProvider({
|
||||
recipes: {
|
||||
mask: ({ length }) => '•'.repeat(length),
|
||||
none: identity,
|
||||
},
|
||||
}),
|
||||
{
|
||||
provide: TUI_MEDIA,
|
||||
useValue: {
|
||||
mobile: 1000,
|
||||
desktopSmall: 1280,
|
||||
desktopLarge: Infinity,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
36
web/projects/ui/src/app/app.module.ts
Normal file
36
web/projects/ui/src/app/app.module.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
provideHttpClient,
|
||||
withFetch,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||
import { TuiRoot } from '@taiga-ui/core'
|
||||
import { ToastContainerComponent } from 'src/app/components/toast-container.component'
|
||||
import { environment } from '../environments/environment'
|
||||
import { AppComponent } from './app.component'
|
||||
import { APP_PROVIDERS } from './app.providers'
|
||||
import { RoutingModule } from './routing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RoutingModule,
|
||||
ToastContainerComponent,
|
||||
TuiRoot,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: environment.useServiceWorker,
|
||||
// Register the ServiceWorker as soon as the application is stable
|
||||
// or after 30 seconds (whichever comes first).
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
APP_PROVIDERS,
|
||||
provideHttpClient(withInterceptorsFromDi(), withFetch()),
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
157
web/projects/ui/src/app/app.providers.ts
Normal file
157
web/projects/ui/src/app/app.providers.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { inject, provideAppInitializer } from '@angular/core'
|
||||
import { UntypedFormBuilder } from '@angular/forms'
|
||||
import { provideAnimations } from '@angular/platform-browser/animations'
|
||||
import { ActivationStart, Router } from '@angular/router'
|
||||
import { WA_LOCATION } from '@ng-web-apis/common'
|
||||
import initArgon from '@start9labs/argon2'
|
||||
import {
|
||||
AbstractCategoryService,
|
||||
FilterPackagesPipe,
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
I18N_PROVIDERS,
|
||||
I18N_STORAGE,
|
||||
i18nService,
|
||||
Languages,
|
||||
RELATIVE_URL,
|
||||
VERSION,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
import { tuiObfuscateOptionsProvider } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TUI_DATE_FORMAT,
|
||||
TUI_DIALOGS_CLOSE,
|
||||
TUI_MEDIA,
|
||||
tuiAlertOptionsProvider,
|
||||
tuiButtonOptionsProvider,
|
||||
tuiDropdownOptionsProvider,
|
||||
tuiNumberFormatProvider,
|
||||
} from '@taiga-ui/core'
|
||||
import { provideEventPlugins } from '@taiga-ui/event-plugins'
|
||||
import {
|
||||
TUI_DATE_TIME_VALUE_TRANSFORMER,
|
||||
TUI_DATE_VALUE_TRANSFORMER,
|
||||
} from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, identity, merge, of, pairwise } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import {
|
||||
PATCH_CACHE,
|
||||
PatchDbSource,
|
||||
} from 'src/app/services/patch-db/patch-db-source'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||
import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { CategoryService } from './services/category.service'
|
||||
import { ClientStorageService } from './services/client-storage.service'
|
||||
import { DateTransformerService } from './services/date-transformer.service'
|
||||
import { DatetimeTransformerService } from './services/datetime-transformer.service'
|
||||
import { StorageService } from './services/storage.service'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
ui: { api },
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
export const APP_PROVIDERS = [
|
||||
provideAnimations(),
|
||||
provideEventPlugins(),
|
||||
I18N_PROVIDERS,
|
||||
FilterPackagesPipe,
|
||||
FilterUpdatesPipe,
|
||||
UntypedFormBuilder,
|
||||
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
tuiDropdownOptionsProvider({ appearance: 'start-os' }),
|
||||
tuiAlertOptionsProvider({
|
||||
autoClose: appearance => (appearance === 'negative' ? 0 : 3000),
|
||||
}),
|
||||
{
|
||||
provide: TUI_DATE_FORMAT,
|
||||
useValue: of({
|
||||
mode: 'MDY',
|
||||
separator: '/',
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: TUI_DATE_VALUE_TRANSFORMER,
|
||||
useClass: DateTransformerService,
|
||||
},
|
||||
{
|
||||
provide: TUI_DATE_TIME_VALUE_TRANSFORMER,
|
||||
useClass: DatetimeTransformerService,
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: PatchDB,
|
||||
deps: [PatchDbSource, PATCH_CACHE],
|
||||
useClass: PatchDB,
|
||||
},
|
||||
provideAppInitializer(() => {
|
||||
const i18n = inject(i18nService)
|
||||
const origin = inject(WA_LOCATION).origin
|
||||
const module_or_path = new URL('/assets/argon2_bg.wasm', origin)
|
||||
|
||||
initArgon({ module_or_path })
|
||||
inject(StorageService).migrate036()
|
||||
inject(AuthService).init()
|
||||
inject(ClientStorageService).init()
|
||||
inject(Router).initialNavigation()
|
||||
i18n.setLanguage(i18n.language || 'english')
|
||||
}),
|
||||
{
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
{
|
||||
provide: AbstractCategoryService,
|
||||
useClass: CategoryService,
|
||||
},
|
||||
{
|
||||
provide: TUI_DIALOGS_CLOSE,
|
||||
useFactory: () =>
|
||||
merge(
|
||||
inject(Router).events.pipe(filter(e => e instanceof ActivationStart)),
|
||||
inject(StateService).pipe(
|
||||
pairwise(),
|
||||
filter(
|
||||
([prev, curr]) =>
|
||||
prev === 'running' &&
|
||||
(curr === 'error' || curr === 'initializing'),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
provide: I18N_STORAGE,
|
||||
useFactory: () => {
|
||||
const api = inject(ApiService)
|
||||
|
||||
return (language: Languages) => api.setLanguage({ language })
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: VERSION,
|
||||
useFactory: () => inject(ConfigService).version,
|
||||
},
|
||||
tuiObfuscateOptionsProvider({
|
||||
recipes: {
|
||||
mask: ({ length }) => '•'.repeat(length),
|
||||
none: identity,
|
||||
},
|
||||
}),
|
||||
{
|
||||
provide: TUI_MEDIA,
|
||||
useValue: {
|
||||
mobile: 1000,
|
||||
desktopSmall: 1280,
|
||||
desktopLarge: Infinity,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadComponent: () => import('./logs.component'),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(ROUTES)],
|
||||
})
|
||||
export default class DiagnosticModule {}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Routes } from '@angular/router'
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () => import('./home/home.page'),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadComponent: () => import('./logs.component'),
|
||||
},
|
||||
] satisfies Routes
|
||||
@@ -0,0 +1,19 @@
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { HomePage } from './home.page'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, TuiButton, RouterModule.forChild(ROUTES), i18nPipe],
|
||||
declarations: [HomePage],
|
||||
})
|
||||
export class HomePageModule {}
|
||||
@@ -1,14 +1,6 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||
import {
|
||||
DialogService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { DialogService, i18nKey, LoadingService } from '@start9labs/shared'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
@@ -17,9 +9,9 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
selector: 'diagnostic-home',
|
||||
templateUrl: 'home.component.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
imports: [CommonModule, TuiButton, i18nPipe, RouterLink],
|
||||
standalone: false,
|
||||
})
|
||||
export default class HomePage {
|
||||
export class HomePage {
|
||||
restarted = false
|
||||
error?: {
|
||||
code: number
|
||||
|
||||
@@ -20,7 +20,9 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
template: '<app-initializing [progress]="progress()" />',
|
||||
template: `
|
||||
<app-initializing [progress]="progress()" />
|
||||
`,
|
||||
providers: [provideSetupLogsService(ApiService)],
|
||||
styles: ':host { height: 100%; }',
|
||||
imports: [InitializingComponent],
|
||||
|
||||
37
web/projects/ui/src/app/routes/login/login.module.ts
Normal file
37
web/projects/ui/src/app/routes/login/login.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiError, TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiPassword } from '@taiga-ui/kit'
|
||||
import { TuiCardLarge } from '@taiga-ui/layout'
|
||||
import { CAWizardComponent } from './ca-wizard/ca-wizard.component'
|
||||
import { LoginPage } from './login.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LoginPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
CAWizardComponent,
|
||||
TuiButton,
|
||||
TuiCardLarge,
|
||||
...TuiTextfield,
|
||||
TuiIcon,
|
||||
TuiPassword,
|
||||
TuiAutoFocus,
|
||||
TuiError,
|
||||
RouterModule.forChild(routes),
|
||||
i18nPipe,
|
||||
],
|
||||
declarations: [LoginPage],
|
||||
})
|
||||
export class LoginPageModule {}
|
||||
@@ -1,38 +1,19 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, DestroyRef, DOCUMENT, inject, Inject } from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import { i18nKey, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiError, TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiPassword } from '@taiga-ui/kit'
|
||||
import { TuiCardLarge } from '@taiga-ui/layout'
|
||||
import { CAWizardComponent } from 'src/app/routes/login/ca-wizard/ca-wizard.component'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { Component, Inject, DestroyRef, inject, DOCUMENT } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { i18nKey, LoadingService } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.page.scss'],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
CAWizardComponent,
|
||||
TuiButton,
|
||||
TuiCardLarge,
|
||||
TuiTextfield,
|
||||
TuiIcon,
|
||||
TuiPassword,
|
||||
TuiAutoFocus,
|
||||
TuiError,
|
||||
i18nPipe,
|
||||
],
|
||||
providers: [],
|
||||
standalone: false,
|
||||
})
|
||||
export default class LoginPage {
|
||||
export class LoginPage {
|
||||
password = ''
|
||||
error: i18nKey | null = null
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface FormContext<T> {
|
||||
buttons: ActionButton<T>[]
|
||||
value?: T
|
||||
operations?: Operation[]
|
||||
note?: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -44,9 +43,6 @@ export interface FormContext<T> {
|
||||
(tuiValueChanges)="markAsDirty()"
|
||||
>
|
||||
<form-group [spec]="spec" />
|
||||
@if (note) {
|
||||
<p class="note">{{ note }}</p>
|
||||
}
|
||||
<footer>
|
||||
<ng-content />
|
||||
@for (button of buttons; track $index) {
|
||||
@@ -74,12 +70,6 @@ export interface FormContext<T> {
|
||||
</form>
|
||||
`,
|
||||
styles: `
|
||||
.note {
|
||||
color: var(--tui-text-secondary);
|
||||
font: var(--tui-font-text-s);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
@@ -116,7 +106,6 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
@Input() buttons = this.context?.data.buttons || []
|
||||
@Input() operations = this.context?.data.operations || []
|
||||
@Input() value?: T = this.context?.data.value
|
||||
@Input() note = this.context?.data.note || ''
|
||||
|
||||
form = new FormGroup({})
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import { HeaderStatusComponent } from './status.component'
|
||||
height: 2.75rem;
|
||||
border-radius: var(--bumper);
|
||||
margin: var(--bumper);
|
||||
clip-path: inset(0 round var(--bumper));
|
||||
overflow: hidden;
|
||||
filter: grayscale(1) brightness(0.75);
|
||||
|
||||
@@ -108,8 +107,7 @@ import { HeaderStatusComponent } from './status.component'
|
||||
|
||||
&:has([data-status='success']) {
|
||||
--status: transparent;
|
||||
// "none" breaks border radius in Firefox
|
||||
filter: grayscale(0.001);
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ import { ABOUT } from './about.component'
|
||||
}
|
||||
<tui-data-list [style.width.rem]="13">
|
||||
<tui-opt-group>
|
||||
<button tuiOption iconStart="@tui.info" new (click)="about()">
|
||||
<button tuiOption iconStart="@tui.info" (click)="about()">
|
||||
{{ 'About this server' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
@@ -53,15 +53,13 @@ import { ABOUT } from './about.component'
|
||||
<a
|
||||
tuiOption
|
||||
docsLink
|
||||
new
|
||||
iconStart="@tui.book-open-text"
|
||||
path="/start-os/user-manual"
|
||||
iconStart="@tui.book-open"
|
||||
path="/start-os/user-manual/index.html"
|
||||
>
|
||||
{{ 'User manual' | i18n }}
|
||||
</a>
|
||||
<a
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.headphones"
|
||||
href="https://start9.com/contact"
|
||||
>
|
||||
@@ -69,7 +67,6 @@ import { ABOUT } from './about.component'
|
||||
</a>
|
||||
<a
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.dollar-sign"
|
||||
href="https://donate.start9.com"
|
||||
>
|
||||
@@ -79,7 +76,6 @@ import { ABOUT } from './about.component'
|
||||
<tui-opt-group label="">
|
||||
<a
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.settings"
|
||||
routerLink="/system"
|
||||
(click)="open = false"
|
||||
@@ -90,7 +86,6 @@ import { ABOUT } from './about.component'
|
||||
<tui-opt-group label="">
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.refresh-cw"
|
||||
(click)="promptPower('restart')"
|
||||
>
|
||||
@@ -98,13 +93,12 @@ import { ABOUT } from './about.component'
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.power"
|
||||
(click)="promptPower('shutdown')"
|
||||
>
|
||||
{{ 'Shutdown' | i18n }}
|
||||
</button>
|
||||
<button tuiOption new iconStart="@tui.log-out" (click)="logout()">
|
||||
<button tuiOption iconStart="@tui.log-out" (click)="logout()">
|
||||
{{ 'Logout' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
|
||||
@@ -30,6 +30,19 @@ import { DomainHealthService } from './domain-health.service'
|
||||
selector: 'td[actions]',
|
||||
template: `
|
||||
<div class="desktop">
|
||||
@if (address().ui) {
|
||||
<a
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[attr.href]="address().enabled ? address().url : null"
|
||||
[class.disabled]="!address().enabled"
|
||||
>
|
||||
{{ 'Open UI' | i18n }}
|
||||
</a>
|
||||
}
|
||||
@if (address().deletable) {
|
||||
<button
|
||||
tuiIconButton
|
||||
@@ -74,19 +87,6 @@ import { DomainHealthService } from './domain-health.service'
|
||||
{{ 'Address Requirements' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (address().ui) {
|
||||
<a
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[attr.href]="address().enabled ? address().url : null"
|
||||
[class.disabled]="!address().enabled"
|
||||
>
|
||||
{{ 'Open UI' | i18n }}
|
||||
</a>
|
||||
}
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
|
||||
@@ -37,7 +37,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
selector: 'section[gatewayGroup]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Gateway' | i18n }}: {{ gatewayGroup().gatewayName }}
|
||||
{{ gatewayGroup().gatewayName }}
|
||||
<button
|
||||
tuiDropdown
|
||||
tuiButton
|
||||
@@ -57,14 +57,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
</button>
|
||||
</header>
|
||||
<table
|
||||
[appTable]="[
|
||||
null,
|
||||
'Access',
|
||||
'Type',
|
||||
'Certificate Authority',
|
||||
'URL',
|
||||
null,
|
||||
]"
|
||||
[appTable]="['Enabled', 'Type', 'Certificate Authority', 'URL', null]"
|
||||
>
|
||||
@for (address of gatewayGroup().addresses; track $index) {
|
||||
<tr
|
||||
@@ -76,7 +69,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
></tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<td colspan="5">
|
||||
<app-placeholder icon="@tui.list-x">
|
||||
{{ 'No addresses' | i18n }}
|
||||
</app-placeholder>
|
||||
@@ -139,7 +132,6 @@ export class InterfaceAddressesComponent {
|
||||
}),
|
||||
}),
|
||||
),
|
||||
note: this.getSharedHostNote(),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save')!,
|
||||
@@ -198,7 +190,6 @@ export class InterfaceAddressesComponent {
|
||||
size: 's',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(addSpec),
|
||||
note: this.getSharedHostNote(),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save')!,
|
||||
@@ -216,22 +207,18 @@ export class InterfaceAddressesComponent {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
let configured: boolean
|
||||
if (this.packageId()) {
|
||||
configured = await this.api.pkgAddPrivateDomain({
|
||||
await this.api.pkgAddPrivateDomain({
|
||||
fqdn,
|
||||
gateway: gatewayId,
|
||||
package: this.packageId(),
|
||||
host: iface?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
configured = await this.api.osUiAddPrivateDomain({
|
||||
fqdn,
|
||||
gateway: gatewayId,
|
||||
})
|
||||
await this.api.osUiAddPrivateDomain({ fqdn, gateway: gatewayId })
|
||||
}
|
||||
|
||||
await this.domainHealth.checkPrivateDomain(gatewayId, configured)
|
||||
await this.domainHealth.checkPrivateDomain(gatewayId)
|
||||
|
||||
return true
|
||||
} catch (e: any) {
|
||||
@@ -242,13 +229,6 @@ export class InterfaceAddressesComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private getSharedHostNote(): string {
|
||||
const names = this.value()?.sharedHostNames
|
||||
if (!names?.length) return ''
|
||||
|
||||
return `${this.i18n.transform('This domain will also apply to')} ${names.join(', ')}`
|
||||
}
|
||||
|
||||
private async savePublicDomain(
|
||||
fqdn: string,
|
||||
authority?: 'local' | string,
|
||||
@@ -261,22 +241,26 @@ export class InterfaceAddressesComponent {
|
||||
fqdn,
|
||||
gateway: gatewayId,
|
||||
acme: !authority || authority === 'local' ? null : authority,
|
||||
internalPort: iface?.addressInfo.internalPort || 80,
|
||||
}
|
||||
|
||||
try {
|
||||
let res
|
||||
if (this.packageId()) {
|
||||
res = await this.api.pkgAddPublicDomain({
|
||||
await this.api.pkgAddPublicDomain({
|
||||
...params,
|
||||
package: this.packageId(),
|
||||
host: iface?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
res = await this.api.osUiAddPublicDomain(params)
|
||||
await this.api.osUiAddPublicDomain(params)
|
||||
}
|
||||
|
||||
await this.domainHealth.checkPublicDomain(fqdn, gatewayId, res)
|
||||
const port = this.gatewayGroup().addresses.find(
|
||||
a => a.access === 'public' && a.hostnameInfo.port !== null,
|
||||
)?.hostnameInfo.port
|
||||
|
||||
if (port !== undefined && port !== null) {
|
||||
await this.domainHealth.checkPublicDomain(fqdn, gatewayId, port)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (e: any) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user