Compare commits
1 Commits
renovate/i
...
7cb0ea30d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cb0ea30d0 |
554
virtweb_backend/Cargo.lock
generated
554
virtweb_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,9 @@ edition = "2024"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.28"
|
log = "0.4.27"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
clap = { version = "4.5.53", features = ["derive", "env"] }
|
clap = { version = "4.5.41", features = ["derive", "env"] }
|
||||||
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] }
|
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] }
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
actix = "0.13.5"
|
actix = "0.13.5"
|
||||||
@@ -17,27 +17,27 @@ actix-remote-ip = "0.1.0"
|
|||||||
actix-session = { version = "0.10.1", features = ["cookie-session"] }
|
actix-session = { version = "0.10.1", features = ["cookie-session"] }
|
||||||
actix-identity = "0.8.0"
|
actix-identity = "0.8.0"
|
||||||
actix-cors = "0.7.1"
|
actix-cors = "0.7.1"
|
||||||
actix-files = "0.6.8"
|
actix-files = "0.6.6"
|
||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
actix-http = "3.11.2"
|
actix-http = "3.11.0"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.141"
|
||||||
serde_yml = "0.0.12"
|
serde_yml = "0.0.12"
|
||||||
quick-xml = { version = "0.38.3", features = ["serialize", "overlapped-lists"] }
|
quick-xml = { version = "0.38.0", features = ["serialize", "overlapped-lists"] }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.98"
|
||||||
actix-multipart = "0.7.2"
|
actix-multipart = "0.7.2"
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.20.0"
|
||||||
reqwest = { version = "0.12.24", features = ["stream"] }
|
reqwest = { version = "0.12.22", features = ["stream"] }
|
||||||
url = "2.5.7"
|
url = "2.5.4"
|
||||||
virt = "0.4.3"
|
virt = "0.4.2"
|
||||||
sysinfo = { version = "0.36.1", features = ["serde"] }
|
sysinfo = { version = "0.36.1", features = ["serde"] }
|
||||||
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
||||||
lazy-regex = "3.4.2"
|
lazy-regex = "3.4.1"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.12"
|
||||||
image = "0.25.9"
|
image = "0.25.6"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
tokio = { version = "1.47.1", features = ["rt", "time", "macros"] }
|
tokio = { version = "1.47.0", features = ["rt", "time", "macros"] }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||||
num = "0.4.3"
|
num = "0.4.3"
|
||||||
@@ -45,5 +45,5 @@ rust-embed = { version = "8.7.2", features = ["mime-guess"] }
|
|||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
nix = { version = "0.30.1", features = ["net"] }
|
nix = { version = "0.30.1", features = ["net"] }
|
||||||
basic-jwt = "0.3.0"
|
basic-jwt = "0.3.0"
|
||||||
zip = "4.3.0"
|
zip = "4.1.0"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.41"
|
||||||
@@ -104,10 +104,10 @@ impl Token {
|
|||||||
|
|
||||||
/// Check whether a token is expired or not
|
/// Check whether a token is expired or not
|
||||||
pub fn is_expired(&self) -> bool {
|
pub fn is_expired(&self) -> bool {
|
||||||
if let Some(max_inactivity) = self.max_inactivity
|
if let Some(max_inactivity) = self.max_inactivity {
|
||||||
&& max_inactivity + self.last_used < time()
|
if max_inactivity + self.last_used < time() {
|
||||||
{
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
@@ -188,10 +188,10 @@ impl NewToken {
|
|||||||
return Some(err);
|
return Some(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(t) = self.max_inactivity
|
if let Some(t) = self.max_inactivity {
|
||||||
&& t < 3600
|
if t < 3600 {
|
||||||
{
|
return Some("API tokens shall be valid for at least 1 hour!");
|
||||||
return Some("API tokens shall be valid for at least 1 hour!");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check file mime type
|
// Check file mime type
|
||||||
if let Some(mime_type) = file.content_type
|
if let Some(mime_type) = file.content_type {
|
||||||
&& !constants::ALLOWED_DISK_IMAGES_MIME_TYPES.contains(&mime_type.as_ref())
|
if !constants::ALLOWED_DISK_IMAGES_MIME_TYPES.contains(&mime_type.as_ref()) {
|
||||||
{
|
return Ok(HttpResponse::BadRequest().json(format!(
|
||||||
return Ok(HttpResponse::BadRequest().json(format!(
|
"Unsupported file type for disk upload: {mime_type}"
|
||||||
"Unsupported file type for disk upload: {mime_type}"
|
)));
|
||||||
)));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and check file name
|
// Extract and check file name
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ pub async fn upload_file(MultipartForm(mut form): MultipartForm<UploadIsoForm>)
|
|||||||
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(m) = &file.content_type
|
if let Some(m) = &file.content_type {
|
||||||
&& !constants::ALLOWED_ISO_MIME_TYPES.contains(&m.to_string().as_str())
|
if !constants::ALLOWED_ISO_MIME_TYPES.contains(&m.to_string().as_str()) {
|
||||||
{
|
log::error!("Uploaded ISO file has an invalid mimetype!");
|
||||||
log::error!("Uploaded ISO file has an invalid mimetype!");
|
return Ok(HttpResponse::BadRequest().json("Invalid mimetype!"));
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid mimetype!"));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_name = match &file.file_name {
|
let file_name = match &file.file_name {
|
||||||
@@ -87,16 +87,16 @@ pub async fn upload_from_url(req: web::Json<DownloadFromURLReq>) -> HttpResult {
|
|||||||
|
|
||||||
let response = reqwest::get(&req.url).await?;
|
let response = reqwest::get(&req.url).await?;
|
||||||
|
|
||||||
if let Some(len) = response.content_length()
|
if let Some(len) = response.content_length() {
|
||||||
&& len > constants::ISO_MAX_SIZE.as_bytes() as u64
|
if len > constants::ISO_MAX_SIZE.as_bytes() as u64 {
|
||||||
{
|
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
||||||
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ct) = response.headers().get("content-type")
|
if let Some(ct) = response.headers().get("content-type") {
|
||||||
&& !constants::ALLOWED_ISO_MIME_TYPES.contains(&ct.to_str()?)
|
if !constants::ALLOWED_ISO_MIME_TYPES.contains(&ct.to_str()?) {
|
||||||
{
|
return Ok(HttpResponse::BadRequest().json("Invalid file mimetype!"));
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid file mimetype!"));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut stream = response.bytes_stream();
|
let mut stream = response.bytes_stream();
|
||||||
|
|||||||
@@ -128,21 +128,21 @@ impl FromRequest for ApiAuthExtractor {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ip) = token.ip_restriction
|
if let Some(ip) = token.ip_restriction {
|
||||||
&& !ip.contains(remote_ip.0)
|
if !ip.contains(remote_ip.0) {
|
||||||
{
|
log::error!(
|
||||||
log::error!(
|
"Attempt to use a token for an unauthorized IP! {remote_ip:?} token_id={}",
|
||||||
"Attempt to use a token for an unauthorized IP! {remote_ip:?} token_id={}",
|
token.id.0
|
||||||
token.id.0
|
);
|
||||||
);
|
return Err(ErrorUnauthorized("Token cannot be used from this IP!"));
|
||||||
return Err(ErrorUnauthorized("Token cannot be used from this IP!"));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if token.should_update_last_activity()
|
if token.should_update_last_activity() {
|
||||||
&& let Err(e) = api_tokens::refresh_last_used(token.id).await
|
if let Err(e) = api_tokens::refresh_last_used(token.id).await {
|
||||||
{
|
log::error!("Could not update token last activity! {e}");
|
||||||
log::error!("Could not update token last activity! {e}");
|
return Err(ErrorBadRequest("Couldn't refresh token last activity!"));
|
||||||
return Err(ErrorBadRequest("Couldn't refresh token last activity!"));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ApiAuthExtractor { token, claims })
|
Ok(ApiAuthExtractor { token, claims })
|
||||||
|
|||||||
@@ -96,28 +96,28 @@ impl NetworkInfo {
|
|||||||
return Err(StructureExtraction("network name is invalid!").into());
|
return Err(StructureExtraction("network name is invalid!").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(n) = &self.title
|
if let Some(n) = &self.title {
|
||||||
&& n.contains('\n')
|
if n.contains('\n') {
|
||||||
{
|
return Err(StructureExtraction("Network title contain newline char!").into());
|
||||||
return Err(StructureExtraction("Network title contain newline char!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dev) = &self.device
|
if let Some(dev) = &self.device {
|
||||||
&& !regex!("^[a-zA-Z0-9]+$").is_match(dev)
|
if !regex!("^[a-zA-Z0-9]+$").is_match(dev) {
|
||||||
{
|
return Err(StructureExtraction("Network device name is invalid!").into());
|
||||||
return Err(StructureExtraction("Network device name is invalid!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(bridge) = &self.bridge_name
|
if let Some(bridge) = &self.bridge_name {
|
||||||
&& !regex!("^[a-zA-Z0-9]+$").is_match(bridge)
|
if !regex!("^[a-zA-Z0-9]+$").is_match(bridge) {
|
||||||
{
|
return Err(StructureExtraction("Network bridge name is invalid!").into());
|
||||||
return Err(StructureExtraction("Network bridge name is invalid!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(domain) = &self.domain
|
if let Some(domain) = &self.domain {
|
||||||
&& !regex!("^[a-zA-Z0-9.]+$").is_match(domain)
|
if !regex!("^[a-zA-Z0-9.]+$").is_match(domain) {
|
||||||
{
|
return Err(StructureExtraction("Domain name is invalid!").into());
|
||||||
return Err(StructureExtraction("Domain name is invalid!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ips = Vec::with_capacity(2);
|
let mut ips = Vec::with_capacity(2);
|
||||||
@@ -303,16 +303,16 @@ impl NetworkInfo {
|
|||||||
|
|
||||||
/// Check if at least one NAT definition was specified on this interface
|
/// Check if at least one NAT definition was specified on this interface
|
||||||
pub fn has_nat_def(&self) -> bool {
|
pub fn has_nat_def(&self) -> bool {
|
||||||
if let Some(ipv4) = &self.ip_v4
|
if let Some(ipv4) = &self.ip_v4 {
|
||||||
&& ipv4.nat.is_some()
|
if ipv4.nat.is_some() {
|
||||||
{
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ipv6) = &self.ip_v6
|
if let Some(ipv6) = &self.ip_v6 {
|
||||||
&& ipv6.nat.is_some()
|
if ipv6.nat.is_some() {
|
||||||
{
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -43,12 +43,14 @@ impl From<&String> for NetworkFilterMacAddressOrVar {
|
|||||||
fn extract_mac_address_or_var(
|
fn extract_mac_address_or_var(
|
||||||
n: &Option<NetworkFilterMacAddressOrVar>,
|
n: &Option<NetworkFilterMacAddressOrVar>,
|
||||||
) -> anyhow::Result<Option<String>> {
|
) -> anyhow::Result<Option<String>> {
|
||||||
if let Some(mac) = n
|
if let Some(mac) = n {
|
||||||
&& !mac.is_valid()
|
if !mac.is_valid() {
|
||||||
{
|
return Err(NetworkFilterExtraction(format!(
|
||||||
return Err(
|
"Invalid mac address or variable! {}",
|
||||||
NetworkFilterExtraction(format!("Invalid mac address or variable! {}", mac.0)).into(),
|
mac.0
|
||||||
);
|
))
|
||||||
|
.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(n.as_ref().map(|n| n.0.to_string()))
|
Ok(n.as_ref().map(|n| n.0.to_string()))
|
||||||
@@ -81,34 +83,34 @@ impl<const V: usize> From<&String> for NetworkFilterIPOrVar<V> {
|
|||||||
fn extract_ip_or_var<const V: usize>(
|
fn extract_ip_or_var<const V: usize>(
|
||||||
n: &Option<NetworkFilterIPOrVar<V>>,
|
n: &Option<NetworkFilterIPOrVar<V>>,
|
||||||
) -> anyhow::Result<Option<String>> {
|
) -> anyhow::Result<Option<String>> {
|
||||||
if let Some(ip) = n
|
if let Some(ip) = n {
|
||||||
&& !ip.is_valid()
|
if !ip.is_valid() {
|
||||||
{
|
return Err(NetworkFilterExtraction(format!(
|
||||||
return Err(NetworkFilterExtraction(format!(
|
"Invalid IPv{V} address or variable! {}",
|
||||||
"Invalid IPv{V} address or variable! {}",
|
ip.0
|
||||||
ip.0
|
))
|
||||||
))
|
.into());
|
||||||
.into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(n.as_ref().map(|n| n.0.to_string()))
|
Ok(n.as_ref().map(|n| n.0.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_ip_mask<const V: usize>(n: Option<u8>) -> anyhow::Result<Option<u8>> {
|
fn extract_ip_mask<const V: usize>(n: Option<u8>) -> anyhow::Result<Option<u8>> {
|
||||||
if let Some(mask) = n
|
if let Some(mask) = n {
|
||||||
&& !net_utils::is_mask_valid(V, mask)
|
if !net_utils::is_mask_valid(V, mask) {
|
||||||
{
|
return Err(NetworkFilterExtraction(format!("Invalid IPv{V} mask! {mask}")).into());
|
||||||
return Err(NetworkFilterExtraction(format!("Invalid IPv{V} mask! {mask}")).into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(n)
|
Ok(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_nw_filter_comment(n: &Option<String>) -> anyhow::Result<Option<String>> {
|
fn extract_nw_filter_comment(n: &Option<String>) -> anyhow::Result<Option<String>> {
|
||||||
if let Some(comment) = n
|
if let Some(comment) = n {
|
||||||
&& (comment.len() > 256 || comment.contains('\"') || comment.contains('\n'))
|
if comment.len() > 256 || comment.contains('\"') || comment.contains('\n') {
|
||||||
{
|
return Err(NetworkFilterExtraction(format!("Invalid comment! {comment}")).into());
|
||||||
return Err(NetworkFilterExtraction(format!("Invalid comment! {comment}")).into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(n.clone())
|
Ok(n.clone())
|
||||||
@@ -867,10 +869,12 @@ impl NetworkFilter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(priority) = self.priority
|
if let Some(priority) = self.priority {
|
||||||
&& !(-1000..=1000).contains(&priority)
|
if !(-1000..=1000).contains(&priority) {
|
||||||
{
|
return Err(
|
||||||
return Err(NetworkFilterExtraction("Network priority is invalid!".to_string()).into());
|
NetworkFilterExtraction("Network priority is invalid!".to_string()).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for fref in &self.join_filters {
|
for fref in &self.join_filters {
|
||||||
|
|||||||
@@ -118,22 +118,22 @@ impl VMInfo {
|
|||||||
XMLUuid::new_random()
|
XMLUuid::new_random()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(n) = &self.genid
|
if let Some(n) = &self.genid {
|
||||||
&& !n.is_valid()
|
if !n.is_valid() {
|
||||||
{
|
return Err(StructureExtraction("VM genid is invalid!").into());
|
||||||
return Err(StructureExtraction("VM genid is invalid!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(n) = &self.title
|
if let Some(n) = &self.title {
|
||||||
&& n.contains('\n')
|
if n.contains('\n') {
|
||||||
{
|
return Err(StructureExtraction("VM title contain newline char!").into());
|
||||||
return Err(StructureExtraction("VM title contain newline char!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(group) = &self.group
|
if let Some(group) = &self.group {
|
||||||
&& !regex!("^[a-zA-Z0-9]+$").is_match(&group.0)
|
if !regex!("^[a-zA-Z0-9]+$").is_match(&group.0) {
|
||||||
{
|
return Err(StructureExtraction("VM group name is invalid!").into());
|
||||||
return Err(StructureExtraction("VM group name is invalid!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY {
|
if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY {
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ pub struct Nat<IPv> {
|
|||||||
|
|
||||||
impl<IPv> Nat<IPv> {
|
impl<IPv> Nat<IPv> {
|
||||||
pub fn check(&self) -> anyhow::Result<()> {
|
pub fn check(&self) -> anyhow::Result<()> {
|
||||||
if let NatSourceIP::Interface { name } = &self.host_ip
|
if let NatSourceIP::Interface { name } = &self.host_ip {
|
||||||
&& !net_utils::is_net_interface_name_valid(name)
|
if !net_utils::is_net_interface_name_valid(name) {
|
||||||
{
|
return Err(NatDefError::InvalidNatDef("Invalid nat interface name!").into());
|
||||||
return Err(NatDefError::InvalidNatDef("Invalid nat interface name!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let NatHostPort::Range { start, end } = &self.host_port {
|
if let NatHostPort::Range { start, end } = &self.host_port {
|
||||||
@@ -84,10 +84,10 @@ impl<IPv> Nat<IPv> {
|
|||||||
return Err(NatDefError::InvalidNatDef("Invalid guest port!").into());
|
return Err(NatDefError::InvalidNatDef("Invalid guest port!").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(comment) = &self.comment
|
if let Some(comment) = &self.comment {
|
||||||
&& comment.len() > constants::NET_NAT_COMMENT_MAX_SIZE
|
if comment.len() > constants::NET_NAT_COMMENT_MAX_SIZE {
|
||||||
{
|
return Err(NatDefError::InvalidNatDef("Comment is too large!").into());
|
||||||
return Err(NatDefError::InvalidNatDef("Comment is too large!").into());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
1643
virtweb_frontend/package-lock.json
generated
1643
virtweb_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,45 +12,45 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@fontsource/roboto": "^5.2.8",
|
"@fontsource/roboto": "^5.2.6",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.2.0",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.2.0",
|
||||||
"@mui/x-charts": "^8.3.1",
|
"@mui/x-charts": "^8.3.1",
|
||||||
"@mui/x-data-grid": "^8.17.0",
|
"@mui/x-data-grid": "^8.9.1",
|
||||||
"date-and-time": "^3.6.0",
|
"date-and-time": "^3.6.0",
|
||||||
"filesize": "^10.1.6",
|
"filesize": "^10.1.6",
|
||||||
"humanize-duration": "^3.33.1",
|
"humanize-duration": "^3.33.0",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"monaco-yaml": "^5.4.0",
|
"monaco-yaml": "^5.4.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.6",
|
"react-router-dom": "^7.7.1",
|
||||||
"react-syntax-highlighter": "^15.6.6",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"react-vnc": "^3.1.0",
|
"react-vnc": "^3.1.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"xml-formatter": "^3.6.6",
|
"xml-formatter": "^3.6.6",
|
||||||
"yaml": "^2.8.1"
|
"yaml": "^2.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.32.0",
|
||||||
"@types/humanize-duration": "^3.27.4",
|
"@types/humanize-duration": "^3.27.4",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "^19.2.6",
|
"@types/react": "^19.1.9",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.32.0",
|
||||||
"eslint-plugin-react-dom": "^1.53.1",
|
"eslint-plugin-react-dom": "^1.52.3",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"eslint-plugin-react-x": "^1.53.1",
|
"eslint-plugin-react-x": "^1.52.3",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.43.0",
|
"typescript-eslint": "^8.38.0",
|
||||||
"vite": "^6.3.6"
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user