Can upload ISO files
This commit is contained in:
		
							
								
								
									
										1
									
								
								virtweb_backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								virtweb_backend/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,3 @@
 | 
				
			|||||||
target/
 | 
					target/
 | 
				
			||||||
.idea
 | 
					.idea
 | 
				
			||||||
 | 
					storage
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										96
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										96
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -99,6 +99,44 @@ dependencies = [
 | 
				
			|||||||
 "syn 2.0.29",
 | 
					 "syn 2.0.29",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "actix-multipart"
 | 
				
			||||||
 | 
					version = "0.6.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "actix-multipart-derive",
 | 
				
			||||||
 | 
					 "actix-utils",
 | 
				
			||||||
 | 
					 "actix-web",
 | 
				
			||||||
 | 
					 "bytes",
 | 
				
			||||||
 | 
					 "derive_more",
 | 
				
			||||||
 | 
					 "futures-core",
 | 
				
			||||||
 | 
					 "futures-util",
 | 
				
			||||||
 | 
					 "httparse",
 | 
				
			||||||
 | 
					 "local-waker",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "memchr",
 | 
				
			||||||
 | 
					 "mime",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "serde_json",
 | 
				
			||||||
 | 
					 "serde_plain",
 | 
				
			||||||
 | 
					 "tempfile",
 | 
				
			||||||
 | 
					 "tokio",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "actix-multipart-derive"
 | 
				
			||||||
 | 
					version = "0.6.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "darling",
 | 
				
			||||||
 | 
					 "parse-size",
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn 2.0.29",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "actix-remote-ip"
 | 
					name = "actix-remote-ip"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
@@ -656,6 +694,41 @@ dependencies = [
 | 
				
			|||||||
 "cipher",
 | 
					 "cipher",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "darling"
 | 
				
			||||||
 | 
					version = "0.20.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "darling_core",
 | 
				
			||||||
 | 
					 "darling_macro",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "darling_core"
 | 
				
			||||||
 | 
					version = "0.20.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "fnv",
 | 
				
			||||||
 | 
					 "ident_case",
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "strsim",
 | 
				
			||||||
 | 
					 "syn 2.0.29",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "darling_macro"
 | 
				
			||||||
 | 
					version = "0.20.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "darling_core",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn 2.0.29",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "deranged"
 | 
					name = "deranged"
 | 
				
			||||||
version = "0.3.8"
 | 
					version = "0.3.8"
 | 
				
			||||||
@@ -996,6 +1069,12 @@ dependencies = [
 | 
				
			|||||||
 "tokio-native-tls",
 | 
					 "tokio-native-tls",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "ident_case"
 | 
				
			||||||
 | 
					version = "1.0.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "idna"
 | 
					name = "idna"
 | 
				
			||||||
version = "0.4.0"
 | 
					version = "0.4.0"
 | 
				
			||||||
@@ -1280,6 +1359,12 @@ dependencies = [
 | 
				
			|||||||
 "windows-targets",
 | 
					 "windows-targets",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "parse-size"
 | 
				
			||||||
 | 
					version = "1.0.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "paste"
 | 
					name = "paste"
 | 
				
			||||||
version = "1.0.14"
 | 
					version = "1.0.14"
 | 
				
			||||||
@@ -1560,6 +1645,15 @@ dependencies = [
 | 
				
			|||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "serde_plain"
 | 
				
			||||||
 | 
					version = "1.0.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "serde_urlencoded"
 | 
					name = "serde_urlencoded"
 | 
				
			||||||
version = "0.7.1"
 | 
					version = "0.7.1"
 | 
				
			||||||
@@ -1895,6 +1989,7 @@ version = "0.1.0"
 | 
				
			|||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "actix-cors",
 | 
					 "actix-cors",
 | 
				
			||||||
 "actix-identity",
 | 
					 "actix-identity",
 | 
				
			||||||
 | 
					 "actix-multipart",
 | 
				
			||||||
 "actix-remote-ip",
 | 
					 "actix-remote-ip",
 | 
				
			||||||
 "actix-session",
 | 
					 "actix-session",
 | 
				
			||||||
 "actix-web",
 | 
					 "actix-web",
 | 
				
			||||||
@@ -1907,6 +2002,7 @@ dependencies = [
 | 
				
			|||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
 "serde_json",
 | 
					 "serde_json",
 | 
				
			||||||
 | 
					 "tempfile",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,4 +19,6 @@ actix-cors = "0.6.4"
 | 
				
			|||||||
serde = { version = "1.0.175", features = ["derive"] }
 | 
					serde = { version = "1.0.175", features = ["derive"] }
 | 
				
			||||||
serde_json = "1.0.105"
 | 
					serde_json = "1.0.105"
 | 
				
			||||||
futures-util = "0.3.28"
 | 
					futures-util = "0.3.28"
 | 
				
			||||||
anyhow = "1.0.75"
 | 
					anyhow = "1.0.75"
 | 
				
			||||||
 | 
					actix-multipart = "0.6.1"
 | 
				
			||||||
 | 
					tempfile = "3.8.0"
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
use clap::Parser;
 | 
					use clap::Parser;
 | 
				
			||||||
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// VirtWeb backend API
 | 
					/// VirtWeb backend API
 | 
				
			||||||
#[derive(Parser, Debug, Clone)]
 | 
					#[derive(Parser, Debug, Clone)]
 | 
				
			||||||
@@ -64,6 +65,10 @@ pub struct AppConfig {
 | 
				
			|||||||
    #[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
 | 
					    #[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
 | 
				
			||||||
    oidc_redirect_url: String,
 | 
					    oidc_redirect_url: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Storage directory
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "storage")]
 | 
				
			||||||
 | 
					    pub storage: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Directory where temporary files are stored
 | 
					    /// Directory where temporary files are stored
 | 
				
			||||||
    #[arg(long, env, default_value = "/tmp")]
 | 
					    #[arg(long, env, default_value = "/tmp")]
 | 
				
			||||||
    pub temp_dir: String,
 | 
					    pub temp_dir: String,
 | 
				
			||||||
@@ -131,6 +136,16 @@ impl AppConfig {
 | 
				
			|||||||
        self.oidc_redirect_url
 | 
					        self.oidc_redirect_url
 | 
				
			||||||
            .replace("APP_ORIGIN", &self.website_origin)
 | 
					            .replace("APP_ORIGIN", &self.website_origin)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get root storage directory
 | 
				
			||||||
 | 
					    pub fn storage_path(&self) -> PathBuf {
 | 
				
			||||||
 | 
					        Path::new(&self.storage).canonicalize().unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get iso storage directory
 | 
				
			||||||
 | 
					    pub fn iso_storage_path(&self) -> PathBuf {
 | 
				
			||||||
 | 
					        self.storage_path().join("iso")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone, serde::Serialize)]
 | 
					#[derive(Debug, Clone, serde::Serialize)]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,3 +15,9 @@ pub const ROUTES_WITHOUT_AUTH: [&str; 5] = [
 | 
				
			|||||||
    "/api/auth/start_oidc",
 | 
					    "/api/auth/start_oidc",
 | 
				
			||||||
    "/api/auth/finish_oidc",
 | 
					    "/api/auth/finish_oidc",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Allowed ISO mimetypes
 | 
				
			||||||
 | 
					pub const ALLOWED_ISO_MIME_TYPES: [&str; 1] = ["application/x-cd-image"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// ISO max size
 | 
				
			||||||
 | 
					pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										60
									
								
								virtweb_backend/src/controllers/iso_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								virtweb_backend/src/controllers/iso_controller.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
 | 
					use crate::constants;
 | 
				
			||||||
 | 
					use crate::controllers::HttpResult;
 | 
				
			||||||
 | 
					use crate::utils::files_utils;
 | 
				
			||||||
 | 
					use actix_multipart::form::tempfile::TempFile;
 | 
				
			||||||
 | 
					use actix_multipart::form::MultipartForm;
 | 
				
			||||||
 | 
					use actix_web::HttpResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, MultipartForm)]
 | 
				
			||||||
 | 
					pub struct UploadIsoForm {
 | 
				
			||||||
 | 
					    #[multipart(rename = "file")]
 | 
				
			||||||
 | 
					    files: Vec<TempFile>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Upload iso file
 | 
				
			||||||
 | 
					pub async fn upload_file(MultipartForm(mut form): MultipartForm<UploadIsoForm>) -> HttpResult {
 | 
				
			||||||
 | 
					    if form.files.is_empty() {
 | 
				
			||||||
 | 
					        log::error!("Missing uploaded ISO file!");
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Missing file!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let file = form.files.remove(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if file.size > constants::ISO_MAX_SIZE {
 | 
				
			||||||
 | 
					        log::error!("Uploaded ISO file is too large!");
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("File is too large!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(m) = &file.content_type {
 | 
				
			||||||
 | 
					        if !constants::ALLOWED_ISO_MIME_TYPES.contains(&m.to_string().as_str()) {
 | 
				
			||||||
 | 
					            log::error!("Uploaded ISO file has an invalid mimetype!");
 | 
				
			||||||
 | 
					            return Ok(HttpResponse::BadRequest().json("Invalid mimetype!"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let file_name = match &file.file_name {
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            log::error!("Uploaded ISO file does not have a name!");
 | 
				
			||||||
 | 
					            return Ok(HttpResponse::BadRequest().json("Missing file name!"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Some(f) => f,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if !files_utils::check_file_name(file_name) {
 | 
				
			||||||
 | 
					        log::error!("Bad file name for uploaded iso!");
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Bad file name!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let dest_file = AppConfig::get().iso_storage_path().join(file_name);
 | 
				
			||||||
 | 
					    log::info!("Will save ISO file {:?}", dest_file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if dest_file.exists() {
 | 
				
			||||||
 | 
					        log::error!("Conflict with uploaded iso file name!");
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::Conflict().json("The file already exists!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    file.file.persist(dest_file)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(HttpResponse::Accepted().finish())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,6 +5,7 @@ use std::fmt::{Display, Formatter};
 | 
				
			|||||||
use std::io::ErrorKind;
 | 
					use std::io::ErrorKind;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod auth_controller;
 | 
					pub mod auth_controller;
 | 
				
			||||||
 | 
					pub mod iso_controller;
 | 
				
			||||||
pub mod server_controller;
 | 
					pub mod server_controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Custom error to ease controller writing
 | 
					/// Custom error to ease controller writing
 | 
				
			||||||
@@ -58,4 +59,10 @@ impl From<std::num::ParseIntError> for HttpErr {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<tempfile::PersistError> for HttpErr {
 | 
				
			||||||
 | 
					    fn from(value: tempfile::PersistError) -> Self {
 | 
				
			||||||
 | 
					        HttpErr { err: value.into() }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub type HttpResult = Result<HttpResponse, HttpErr>;
 | 
					pub type HttpResult = Result<HttpResponse, HttpErr>;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
use crate::app_config::AppConfig;
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
 | 
					use crate::constants;
 | 
				
			||||||
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
 | 
					use crate::extractors::local_auth_extractor::LocalAuthEnabled;
 | 
				
			||||||
use actix_web::{HttpResponse, Responder};
 | 
					use actix_web::{HttpResponse, Responder};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,11 +11,15 @@ pub async fn root_index() -> impl Responder {
 | 
				
			|||||||
struct StaticConfig {
 | 
					struct StaticConfig {
 | 
				
			||||||
    local_auth_enabled: bool,
 | 
					    local_auth_enabled: bool,
 | 
				
			||||||
    oidc_auth_enabled: bool,
 | 
					    oidc_auth_enabled: bool,
 | 
				
			||||||
 | 
					    iso_mimetypes: &'static [&'static str],
 | 
				
			||||||
 | 
					    iso_max_size: usize,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
					pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
				
			||||||
    HttpResponse::Ok().json(StaticConfig {
 | 
					    HttpResponse::Ok().json(StaticConfig {
 | 
				
			||||||
        local_auth_enabled: *local_auth,
 | 
					        local_auth_enabled: *local_auth,
 | 
				
			||||||
        oidc_auth_enabled: !AppConfig::get().disable_oidc,
 | 
					        oidc_auth_enabled: !AppConfig::get().disable_oidc,
 | 
				
			||||||
 | 
					        iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
 | 
				
			||||||
 | 
					        iso_max_size: constants::ISO_MAX_SIZE,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,3 +3,4 @@ pub mod constants;
 | 
				
			|||||||
pub mod controllers;
 | 
					pub mod controllers;
 | 
				
			||||||
pub mod extractors;
 | 
					pub mod extractors;
 | 
				
			||||||
pub mod middlewares;
 | 
					pub mod middlewares;
 | 
				
			||||||
 | 
					pub mod utils;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
use actix_cors::Cors;
 | 
					use actix_cors::Cors;
 | 
				
			||||||
use actix_identity::config::LogoutBehaviour;
 | 
					use actix_identity::config::LogoutBehaviour;
 | 
				
			||||||
use actix_identity::IdentityMiddleware;
 | 
					use actix_identity::IdentityMiddleware;
 | 
				
			||||||
 | 
					use actix_multipart::form::tempfile::TempFileConfig;
 | 
				
			||||||
 | 
					use actix_multipart::form::MultipartFormConfig;
 | 
				
			||||||
use actix_remote_ip::RemoteIPConfig;
 | 
					use actix_remote_ip::RemoteIPConfig;
 | 
				
			||||||
use actix_session::storage::CookieSessionStore;
 | 
					use actix_session::storage::CookieSessionStore;
 | 
				
			||||||
use actix_session::SessionMiddleware;
 | 
					use actix_session::SessionMiddleware;
 | 
				
			||||||
@@ -12,16 +14,21 @@ use actix_web::{web, App, HttpServer};
 | 
				
			|||||||
use light_openid::basic_state_manager::BasicStateManager;
 | 
					use light_openid::basic_state_manager::BasicStateManager;
 | 
				
			||||||
use std::time::Duration;
 | 
					use std::time::Duration;
 | 
				
			||||||
use virtweb_backend::app_config::AppConfig;
 | 
					use virtweb_backend::app_config::AppConfig;
 | 
				
			||||||
 | 
					use virtweb_backend::constants;
 | 
				
			||||||
use virtweb_backend::constants::{
 | 
					use virtweb_backend::constants::{
 | 
				
			||||||
    MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
 | 
					    MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use virtweb_backend::controllers::{auth_controller, server_controller};
 | 
					use virtweb_backend::controllers::{auth_controller, iso_controller, server_controller};
 | 
				
			||||||
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
 | 
					use virtweb_backend::middlewares::auth_middleware::AuthChecker;
 | 
				
			||||||
 | 
					use virtweb_backend::utils::files_utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[actix_web::main]
 | 
					#[actix_web::main]
 | 
				
			||||||
async fn main() -> std::io::Result<()> {
 | 
					async fn main() -> std::io::Result<()> {
 | 
				
			||||||
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
 | 
					    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log::debug!("Create required directory, if missing");
 | 
				
			||||||
 | 
					    files_utils::create_directory_if_missing(&AppConfig::get().iso_storage_path()).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log::info!("Start to listen on {}", AppConfig::get().listen_address);
 | 
					    log::info!("Start to listen on {}", AppConfig::get().listen_address);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let state_manager = Data::new(BasicStateManager::new());
 | 
					    let state_manager = Data::new(BasicStateManager::new());
 | 
				
			||||||
@@ -62,6 +69,10 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
            .app_data(Data::new(RemoteIPConfig {
 | 
					            .app_data(Data::new(RemoteIPConfig {
 | 
				
			||||||
                proxy: AppConfig::get().proxy_ip.clone(),
 | 
					                proxy: AppConfig::get().proxy_ip.clone(),
 | 
				
			||||||
            }))
 | 
					            }))
 | 
				
			||||||
 | 
					            // Uploaded files
 | 
				
			||||||
 | 
					            .app_data(web::PayloadConfig::new(constants::ISO_MAX_SIZE))
 | 
				
			||||||
 | 
					            .app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE))
 | 
				
			||||||
 | 
					            .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
 | 
				
			||||||
            // Server controller
 | 
					            // Server controller
 | 
				
			||||||
            .route("/", web::get().to(server_controller::root_index))
 | 
					            .route("/", web::get().to(server_controller::root_index))
 | 
				
			||||||
            .route(
 | 
					            .route(
 | 
				
			||||||
@@ -89,6 +100,11 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
                "/api/auth/sign_out",
 | 
					                "/api/auth/sign_out",
 | 
				
			||||||
                web::get().to(auth_controller::sign_out),
 | 
					                web::get().to(auth_controller::sign_out),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            // ISO controller
 | 
				
			||||||
 | 
					            .route(
 | 
				
			||||||
 | 
					                "/api/iso/upload",
 | 
				
			||||||
 | 
					                web::post().to(iso_controller::upload_file),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .bind(&AppConfig::get().listen_address)?
 | 
					    .bind(&AppConfig::get().listen_address)?
 | 
				
			||||||
    .run()
 | 
					    .run()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										49
									
								
								virtweb_backend/src/utils/files_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								virtweb_backend/src/utils/files_utils.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const INVALID_CHARS: [&str; 19] = [
 | 
				
			||||||
 | 
					    "@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=",
 | 
				
			||||||
 | 
					    "\t",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Check out whether a file name is valid or not
 | 
				
			||||||
 | 
					pub fn check_file_name(name: &str) -> bool {
 | 
				
			||||||
 | 
					    !name.is_empty() && !INVALID_CHARS.iter().any(|c| name.contains(c))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create directory if missing
 | 
				
			||||||
 | 
					pub fn create_directory_if_missing(path: &PathBuf) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					    if !path.exists() {
 | 
				
			||||||
 | 
					        std::fs::create_dir_all(path)?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use crate::utils::files_utils::check_file_name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn empty_file_name() {
 | 
				
			||||||
 | 
					        assert!(!check_file_name(""));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parent_dir_file_name() {
 | 
				
			||||||
 | 
					        assert!(!check_file_name("../file.test"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn windows_parent_dir_file_name() {
 | 
				
			||||||
 | 
					        assert!(!check_file_name("..\\test.fr"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn special_char_file_name() {
 | 
				
			||||||
 | 
					        assert!(!check_file_name("test:test.@"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn valid_file_name() {
 | 
				
			||||||
 | 
					        assert!(check_file_name("test.iso"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								virtweb_backend/src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								virtweb_backend/src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					pub mod files_utils;
 | 
				
			||||||
							
								
								
									
										77
									
								
								virtweb_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										77
									
								
								virtweb_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -22,6 +22,8 @@
 | 
				
			|||||||
        "@types/node": "^16.18.48",
 | 
					        "@types/node": "^16.18.48",
 | 
				
			||||||
        "@types/react": "^18.2.21",
 | 
					        "@types/react": "^18.2.21",
 | 
				
			||||||
        "@types/react-dom": "^18.2.7",
 | 
					        "@types/react-dom": "^18.2.7",
 | 
				
			||||||
 | 
					        "filesize": "^10.0.12",
 | 
				
			||||||
 | 
					        "mui-file-input": "^3.0.1",
 | 
				
			||||||
        "react": "^18.2.0",
 | 
					        "react": "^18.2.0",
 | 
				
			||||||
        "react-dom": "^18.2.0",
 | 
					        "react-dom": "^18.2.0",
 | 
				
			||||||
        "react-router-dom": "^6.15.0",
 | 
					        "react-router-dom": "^6.15.0",
 | 
				
			||||||
@@ -8472,11 +8474,11 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/filesize": {
 | 
					    "node_modules/filesize": {
 | 
				
			||||||
      "version": "8.0.7",
 | 
					      "version": "10.0.12",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.12.tgz",
 | 
				
			||||||
      "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==",
 | 
					      "integrity": "sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==",
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">= 0.4.0"
 | 
					        "node": ">= 10.4.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/fill-range": {
 | 
					    "node_modules/fill-range": {
 | 
				
			||||||
@@ -12673,6 +12675,39 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
 | 
					      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/mui-file-input": {
 | 
				
			||||||
 | 
					      "version": "3.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/mui-file-input/-/mui-file-input-3.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-02MtRtyPALqcQubpMdBVhScNK9T28GBcjJwv/Wg33hjJeEQzYaCiVC5OIjMO8WFoH5gEa+lYSfZAZ4rnn5r5cw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "pretty-bytes": "^6.1.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "@emotion/react": "^11.5.0",
 | 
				
			||||||
 | 
					        "@emotion/styled": "^11.3.0",
 | 
				
			||||||
 | 
					        "@mui/icons-material": "^5.0.0",
 | 
				
			||||||
 | 
					        "@mui/material": "^5.0.0",
 | 
				
			||||||
 | 
					        "@types/react": "^18.0.0",
 | 
				
			||||||
 | 
					        "react": "^18.0.0",
 | 
				
			||||||
 | 
					        "react-dom": "^18.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependenciesMeta": {
 | 
				
			||||||
 | 
					        "@types/react": {
 | 
				
			||||||
 | 
					          "optional": true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/mui-file-input/node_modules/pretty-bytes": {
 | 
				
			||||||
 | 
					      "version": "6.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": "^14.13.1 || >=16.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/sponsors/sindresorhus"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/multicast-dns": {
 | 
					    "node_modules/multicast-dns": {
 | 
				
			||||||
      "version": "7.2.5",
 | 
					      "version": "7.2.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
 | 
				
			||||||
@@ -14886,6 +14921,14 @@
 | 
				
			|||||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
					        "url": "https://github.com/sponsors/sindresorhus"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-dev-utils/node_modules/filesize": {
 | 
				
			||||||
 | 
					      "version": "8.0.7",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 0.4.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/react-dev-utils/node_modules/has-flag": {
 | 
					    "node_modules/react-dev-utils/node_modules/has-flag": {
 | 
				
			||||||
      "version": "4.0.0",
 | 
					      "version": "4.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 | 
				
			||||||
@@ -23995,9 +24038,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "filesize": {
 | 
					    "filesize": {
 | 
				
			||||||
      "version": "8.0.7",
 | 
					      "version": "10.0.12",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.12.tgz",
 | 
				
			||||||
      "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ=="
 | 
					      "integrity": "sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "fill-range": {
 | 
					    "fill-range": {
 | 
				
			||||||
      "version": "7.0.1",
 | 
					      "version": "7.0.1",
 | 
				
			||||||
@@ -27010,6 +27053,21 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
 | 
					      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "mui-file-input": {
 | 
				
			||||||
 | 
					      "version": "3.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/mui-file-input/-/mui-file-input-3.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-02MtRtyPALqcQubpMdBVhScNK9T28GBcjJwv/Wg33hjJeEQzYaCiVC5OIjMO8WFoH5gEa+lYSfZAZ4rnn5r5cw==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "pretty-bytes": "^6.1.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "pretty-bytes": {
 | 
				
			||||||
 | 
					          "version": "6.1.1",
 | 
				
			||||||
 | 
					          "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
 | 
				
			||||||
 | 
					          "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "multicast-dns": {
 | 
					    "multicast-dns": {
 | 
				
			||||||
      "version": "7.2.5",
 | 
					      "version": "7.2.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
 | 
				
			||||||
@@ -28422,6 +28480,11 @@
 | 
				
			|||||||
          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
 | 
					          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
 | 
				
			||||||
          "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
 | 
					          "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "filesize": {
 | 
				
			||||||
 | 
					          "version": "8.0.7",
 | 
				
			||||||
 | 
					          "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
 | 
				
			||||||
 | 
					          "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ=="
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "has-flag": {
 | 
					        "has-flag": {
 | 
				
			||||||
          "version": "4.0.0",
 | 
					          "version": "4.0.0",
 | 
				
			||||||
          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 | 
					          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,8 @@
 | 
				
			|||||||
    "@types/node": "^16.18.48",
 | 
					    "@types/node": "^16.18.48",
 | 
				
			||||||
    "@types/react": "^18.2.21",
 | 
					    "@types/react": "^18.2.21",
 | 
				
			||||||
    "@types/react-dom": "^18.2.7",
 | 
					    "@types/react-dom": "^18.2.7",
 | 
				
			||||||
 | 
					    "filesize": "^10.0.12",
 | 
				
			||||||
 | 
					    "mui-file-input": "^3.0.1",
 | 
				
			||||||
    "react": "^18.2.0",
 | 
					    "react": "^18.2.0",
 | 
				
			||||||
    "react-dom": "^18.2.0",
 | 
					    "react-dom": "^18.2.0",
 | 
				
			||||||
    "react-router-dom": "^6.15.0",
 | 
					    "react-router-dom": "^6.15.0",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import { BaseLoginPage } from "./widgets/BaseLoginPage";
 | 
				
			|||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
 | 
					import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
 | 
				
			||||||
import { LoginRoute } from "./routes/auth/LoginRoute";
 | 
					import { LoginRoute } from "./routes/auth/LoginRoute";
 | 
				
			||||||
import { AuthApi } from "./api/AuthApi";
 | 
					import { AuthApi } from "./api/AuthApi";
 | 
				
			||||||
 | 
					import { IsoFilesRoute } from "./routes/IsoFilesRoute";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AuthContext {
 | 
					interface AuthContext {
 | 
				
			||||||
  signedIn: boolean;
 | 
					  signedIn: boolean;
 | 
				
			||||||
@@ -32,6 +33,7 @@ export function App() {
 | 
				
			|||||||
    createRoutesFromElements(
 | 
					    createRoutesFromElements(
 | 
				
			||||||
      signedIn ? (
 | 
					      signedIn ? (
 | 
				
			||||||
        <Route path="*" element={<BaseAuthenticatedPage />}>
 | 
					        <Route path="*" element={<BaseAuthenticatedPage />}>
 | 
				
			||||||
 | 
					          <Route path="iso" element={<IsoFilesRoute />} />
 | 
				
			||||||
          <Route path="*" element={<NotFoundRoute />} />
 | 
					          <Route path="*" element={<NotFoundRoute />} />
 | 
				
			||||||
        </Route>
 | 
					        </Route>
 | 
				
			||||||
      ) : (
 | 
					      ) : (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,14 @@
 | 
				
			|||||||
import { AuthApi } from "./AuthApi";
 | 
					import { AuthApi } from "./AuthApi";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RequestParams {
 | 
				
			||||||
 | 
					  uri: string;
 | 
				
			||||||
 | 
					  method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
 | 
				
			||||||
 | 
					  allowFail?: boolean;
 | 
				
			||||||
 | 
					  jsonData?: any;
 | 
				
			||||||
 | 
					  formData?: FormData;
 | 
				
			||||||
 | 
					  progress?: (progress: number) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface APIResponse {
 | 
					interface APIResponse {
 | 
				
			||||||
  data: any;
 | 
					  data: any;
 | 
				
			||||||
  status: number;
 | 
					  status: number;
 | 
				
			||||||
@@ -32,14 +41,8 @@ export class APIClient {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Perform a request on the backend
 | 
					   * Perform a request on the backend
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static async exec(args: {
 | 
					  static async exec(args: RequestParams): Promise<APIResponse> {
 | 
				
			||||||
    uri: string;
 | 
					    let body: string | undefined | FormData = undefined;
 | 
				
			||||||
    method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
 | 
					 | 
				
			||||||
    allowFail?: boolean;
 | 
					 | 
				
			||||||
    jsonData?: any;
 | 
					 | 
				
			||||||
    formData?: FormData;
 | 
					 | 
				
			||||||
  }): Promise<APIResponse> {
 | 
					 | 
				
			||||||
    let body = undefined;
 | 
					 | 
				
			||||||
    let headers: any = {};
 | 
					    let headers: any = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // JSON request
 | 
					    // JSON request
 | 
				
			||||||
@@ -53,31 +56,71 @@ export class APIClient {
 | 
				
			|||||||
      body = args.formData;
 | 
					      body = args.formData;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = await fetch(this.backendURL() + args.uri, {
 | 
					    const url = this.backendURL() + args.uri;
 | 
				
			||||||
      method: args.method,
 | 
					 | 
				
			||||||
      body: body,
 | 
					 | 
				
			||||||
      headers: headers,
 | 
					 | 
				
			||||||
      credentials: "include",
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Process response
 | 
					 | 
				
			||||||
    let data;
 | 
					    let data;
 | 
				
			||||||
    if (res.headers.get("content-type") === "application/json")
 | 
					    let status: number;
 | 
				
			||||||
      data = await res.json();
 | 
					
 | 
				
			||||||
    else data = await res.blob();
 | 
					    // Make the request with XMLHttpRequest
 | 
				
			||||||
 | 
					    if (args.progress) {
 | 
				
			||||||
 | 
					      const res: XMLHttpRequest = await new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					        const xhr = new XMLHttpRequest();
 | 
				
			||||||
 | 
					        xhr.upload.addEventListener("progress", (e) =>
 | 
				
			||||||
 | 
					          args.progress!(e.loaded / e.total)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        xhr.addEventListener("load", () => resolve(xhr));
 | 
				
			||||||
 | 
					        xhr.addEventListener("error", () =>
 | 
				
			||||||
 | 
					          reject(new Error("File upload failed"))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        xhr.addEventListener("abort", () =>
 | 
				
			||||||
 | 
					          reject(new Error("File upload aborted"))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        xhr.addEventListener("timeout", () =>
 | 
				
			||||||
 | 
					          reject(new Error("File upload timeout"))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        xhr.open(args.method, url, true);
 | 
				
			||||||
 | 
					        xhr.withCredentials = true;
 | 
				
			||||||
 | 
					        for (const key in headers) {
 | 
				
			||||||
 | 
					          if (headers.hasOwnProperty(key))
 | 
				
			||||||
 | 
					            xhr.setRequestHeader(key, headers[key]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        xhr.send(body);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      status = res.status;
 | 
				
			||||||
 | 
					      if (res.responseType === "json") data = JSON.parse(res.responseText);
 | 
				
			||||||
 | 
					      else data = res.response;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Make the request with fetch
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					      const res = await fetch(url, {
 | 
				
			||||||
 | 
					        method: args.method,
 | 
				
			||||||
 | 
					        body: body,
 | 
				
			||||||
 | 
					        headers: headers,
 | 
				
			||||||
 | 
					        credentials: "include",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Process response
 | 
				
			||||||
 | 
					      if (res.headers.get("content-type") === "application/json")
 | 
				
			||||||
 | 
					        data = await res.json();
 | 
				
			||||||
 | 
					      else data = await res.blob();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      status = res.status;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Handle expired tokens
 | 
					    // Handle expired tokens
 | 
				
			||||||
    if (res.status === 412) {
 | 
					    if (status === 412) {
 | 
				
			||||||
      AuthApi.UnsetAuthenticated();
 | 
					      AuthApi.UnsetAuthenticated();
 | 
				
			||||||
      window.location.href = "/";
 | 
					      window.location.href = "/";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!args.allowFail && !res.ok)
 | 
					    if (!args.allowFail && (status < 200 || status > 299))
 | 
				
			||||||
      throw new ApiError("Request failed!", res.status, data);
 | 
					      throw new ApiError("Request failed!", status, data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      data: data,
 | 
					      data: data,
 | 
				
			||||||
      status: res.status,
 | 
					      status: status,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								virtweb_frontend/src/api/IsoFilesApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								virtweb_frontend/src/api/IsoFilesApi.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { APIClient } from "./ApiClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class IsoFilesApi {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Upload a new ISO file to the server
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async Upload(
 | 
				
			||||||
 | 
					    file: File,
 | 
				
			||||||
 | 
					    progress: (progress: number) => void
 | 
				
			||||||
 | 
					  ): Promise<void> {
 | 
				
			||||||
 | 
					    const fd = new FormData();
 | 
				
			||||||
 | 
					    fd.append("file", file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await APIClient.exec({
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      uri: "/iso/upload",
 | 
				
			||||||
 | 
					      formData: fd,
 | 
				
			||||||
 | 
					      progress: progress,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,6 +3,8 @@ import { APIClient } from "./ApiClient";
 | 
				
			|||||||
export interface ServerConfig {
 | 
					export interface ServerConfig {
 | 
				
			||||||
  local_auth_enabled: boolean;
 | 
					  local_auth_enabled: boolean;
 | 
				
			||||||
  oidc_auth_enabled: boolean;
 | 
					  oidc_auth_enabled: boolean;
 | 
				
			||||||
 | 
					  iso_mimetypes: string[];
 | 
				
			||||||
 | 
					  iso_max_size: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let config: ServerConfig | null = null;
 | 
					let config: ServerConfig | null = null;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										68
									
								
								virtweb_frontend/src/hooks/providers/AlertDialogProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								virtweb_frontend/src/hooks/providers/AlertDialogProvider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  DialogActions,
 | 
				
			||||||
 | 
					  DialogContent,
 | 
				
			||||||
 | 
					  DialogContentText,
 | 
				
			||||||
 | 
					  DialogTitle,
 | 
				
			||||||
 | 
					} from "@mui/material";
 | 
				
			||||||
 | 
					import React, { PropsWithChildren } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AlertContext = (message: string, title?: string) => Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AlertContextK = React.createContext<AlertContext | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement {
 | 
				
			||||||
 | 
					  const [open, setOpen] = React.useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [title, setTitle] = React.useState<string | undefined>(undefined);
 | 
				
			||||||
 | 
					  const [message, setMessage] = React.useState("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const cb = React.useRef<null | (() => void)>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClose = () => {
 | 
				
			||||||
 | 
					    setOpen(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (cb.current !== null) cb.current();
 | 
				
			||||||
 | 
					    cb.current = null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const hook: AlertContext = (message, title) => {
 | 
				
			||||||
 | 
					    setTitle(title);
 | 
				
			||||||
 | 
					    setMessage(message);
 | 
				
			||||||
 | 
					    setOpen(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Promise((res) => {
 | 
				
			||||||
 | 
					      cb.current = res;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <AlertContextK.Provider value={hook}>{p.children}</AlertContextK.Provider>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Dialog
 | 
				
			||||||
 | 
					        open={open}
 | 
				
			||||||
 | 
					        onClose={handleClose}
 | 
				
			||||||
 | 
					        aria-labelledby="alert-dialog-title"
 | 
				
			||||||
 | 
					        aria-describedby="alert-dialog-description"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {title && <DialogTitle id="alert-dialog-title">{title}</DialogTitle>}
 | 
				
			||||||
 | 
					        <DialogContent>
 | 
				
			||||||
 | 
					          <DialogContentText id="alert-dialog-description">
 | 
				
			||||||
 | 
					            {message}
 | 
				
			||||||
 | 
					          </DialogContentText>
 | 
				
			||||||
 | 
					        </DialogContent>
 | 
				
			||||||
 | 
					        <DialogActions>
 | 
				
			||||||
 | 
					          <Button onClick={handleClose} autoFocus>
 | 
				
			||||||
 | 
					            Ok
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </DialogActions>
 | 
				
			||||||
 | 
					      </Dialog>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useAlert(): AlertContext {
 | 
				
			||||||
 | 
					  return React.useContext(AlertContextK)!;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								virtweb_frontend/src/hooks/providers/SnackbarProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								virtweb_frontend/src/hooks/providers/SnackbarProvider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					import { Snackbar } from "@mui/material";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import React, { PropsWithChildren } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SnackbarContext = (message: string, duration?: number) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SnackbarContextK = React.createContext<SnackbarContext | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SnackbarProvider(p: PropsWithChildren): React.ReactElement {
 | 
				
			||||||
 | 
					  const [open, setOpen] = React.useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [message, setMessage] = React.useState("");
 | 
				
			||||||
 | 
					  const [duration, setDuration] = React.useState(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClose = () => {
 | 
				
			||||||
 | 
					    setOpen(false);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const hook: SnackbarContext = (message, duration) => {
 | 
				
			||||||
 | 
					    setMessage(message);
 | 
				
			||||||
 | 
					    setDuration(duration ?? 6000);
 | 
				
			||||||
 | 
					    setOpen(true);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <SnackbarContextK.Provider value={hook}>
 | 
				
			||||||
 | 
					        {p.children}
 | 
				
			||||||
 | 
					      </SnackbarContextK.Provider>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Snackbar
 | 
				
			||||||
 | 
					        open={open}
 | 
				
			||||||
 | 
					        autoHideDuration={duration}
 | 
				
			||||||
 | 
					        onClose={handleClose}
 | 
				
			||||||
 | 
					        message={message}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useSnackbar(): SnackbarContext {
 | 
				
			||||||
 | 
					  return React.useContext(SnackbarContextK)!;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -11,6 +11,8 @@ import reportWebVitals from "./reportWebVitals";
 | 
				
			|||||||
import { LoadServerConfig } from "./widgets/LoadServerConfig";
 | 
					import { LoadServerConfig } from "./widgets/LoadServerConfig";
 | 
				
			||||||
import { ThemeProvider, createTheme } from "@mui/material";
 | 
					import { ThemeProvider, createTheme } from "@mui/material";
 | 
				
			||||||
import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
 | 
					import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
 | 
				
			||||||
 | 
					import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
 | 
				
			||||||
 | 
					import { SnackbarProvider } from "./hooks/providers/SnackbarProvider";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const darkTheme = createTheme({
 | 
					const darkTheme = createTheme({
 | 
				
			||||||
  palette: {
 | 
					  palette: {
 | 
				
			||||||
@@ -24,11 +26,15 @@ const root = ReactDOM.createRoot(
 | 
				
			|||||||
root.render(
 | 
					root.render(
 | 
				
			||||||
  <React.StrictMode>
 | 
					  <React.StrictMode>
 | 
				
			||||||
    <ThemeProvider theme={darkTheme}>
 | 
					    <ThemeProvider theme={darkTheme}>
 | 
				
			||||||
      <LoadingMessageProvider>
 | 
					      <AlertDialogProvider>
 | 
				
			||||||
        <LoadServerConfig>
 | 
					        <SnackbarProvider>
 | 
				
			||||||
          <App />
 | 
					          <LoadingMessageProvider>
 | 
				
			||||||
        </LoadServerConfig>
 | 
					            <LoadServerConfig>
 | 
				
			||||||
      </LoadingMessageProvider>
 | 
					              <App />
 | 
				
			||||||
 | 
					            </LoadServerConfig>
 | 
				
			||||||
 | 
					          </LoadingMessageProvider>
 | 
				
			||||||
 | 
					        </SnackbarProvider>{" "}
 | 
				
			||||||
 | 
					      </AlertDialogProvider>
 | 
				
			||||||
    </ThemeProvider>
 | 
					    </ThemeProvider>
 | 
				
			||||||
  </React.StrictMode>
 | 
					  </React.StrictMode>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										90
									
								
								virtweb_frontend/src/routes/IsoFilesRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								virtweb_frontend/src/routes/IsoFilesRoute.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					import { Button, LinearProgress, Typography } from "@mui/material";
 | 
				
			||||||
 | 
					import { filesize } from "filesize";
 | 
				
			||||||
 | 
					import { MuiFileInput } from "mui-file-input";
 | 
				
			||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { IsoFilesApi } from "../api/IsoFilesApi";
 | 
				
			||||||
 | 
					import { ServerApi } from "../api/ServerApi";
 | 
				
			||||||
 | 
					import { useAlert } from "../hooks/providers/AlertDialogProvider";
 | 
				
			||||||
 | 
					import { useSnackbar } from "../hooks/providers/SnackbarProvider";
 | 
				
			||||||
 | 
					import { VirtWebPaper } from "../widgets/VirtWebPaper";
 | 
				
			||||||
 | 
					import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function IsoFilesRoute(): React.ReactElement {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <VirtWebRouteContainer label="ISO files management">
 | 
				
			||||||
 | 
					      <UploadIsoFileForm onFileUploaded={() => alert("file uploaded!")} />
 | 
				
			||||||
 | 
					    </VirtWebRouteContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function UploadIsoFileForm(p: {
 | 
				
			||||||
 | 
					  onFileUploaded: () => void;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const alert = useAlert();
 | 
				
			||||||
 | 
					  const snackbar = useSnackbar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [value, setValue] = React.useState<File | null>(null);
 | 
				
			||||||
 | 
					  const [uploadProgress, setUploadProgress] = React.useState<number | null>(
 | 
				
			||||||
 | 
					    null
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleChange = (newValue: File | null) => {
 | 
				
			||||||
 | 
					    if (newValue && newValue.size > ServerApi.Config.iso_max_size) {
 | 
				
			||||||
 | 
					      alert(
 | 
				
			||||||
 | 
					        `The file is too big (max size allowed: ${filesize(
 | 
				
			||||||
 | 
					          ServerApi.Config.iso_max_size
 | 
				
			||||||
 | 
					        )}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (newValue && !ServerApi.Config.iso_mimetypes.includes(newValue.type)) {
 | 
				
			||||||
 | 
					      alert(`Selected file mimetype is not allowed! (${newValue.type})`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setValue(newValue);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const upload = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      setUploadProgress(0);
 | 
				
			||||||
 | 
					      await IsoFilesApi.Upload(value!, setUploadProgress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setValue(null);
 | 
				
			||||||
 | 
					      snackbar("The file was successfully uploaded!");
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(e);
 | 
				
			||||||
 | 
					      await alert("Failed to perform file upload! " + e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setUploadProgress(null);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (uploadProgress !== null) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <VirtWebPaper label="File upload">
 | 
				
			||||||
 | 
					        <Typography variant="body1">
 | 
				
			||||||
 | 
					          Upload in progress ({Math.floor(uploadProgress * 100)}%)...
 | 
				
			||||||
 | 
					        </Typography>
 | 
				
			||||||
 | 
					        <LinearProgress variant="determinate" value={uploadProgress * 100} />
 | 
				
			||||||
 | 
					      </VirtWebPaper>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <VirtWebPaper label="File upload">
 | 
				
			||||||
 | 
					      <div style={{ display: "flex", alignItems: "center" }}>
 | 
				
			||||||
 | 
					        <span style={{ width: "10px" }}></span>
 | 
				
			||||||
 | 
					        <MuiFileInput
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          onChange={handleChange}
 | 
				
			||||||
 | 
					          style={{ flex: 1 }}
 | 
				
			||||||
 | 
					          inputProps={{ accept: ServerApi.Config.iso_mimetypes.join(",") }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {value && <Button onClick={upload}>Upload file</Button>}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </VirtWebPaper>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								virtweb_frontend/src/widgets/VirtWebPaper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								virtweb_frontend/src/widgets/VirtWebPaper.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { Paper, Typography } from "@mui/material";
 | 
				
			||||||
 | 
					import { PropsWithChildren } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function VirtWebPaper(
 | 
				
			||||||
 | 
					  p: { label: string } & PropsWithChildren
 | 
				
			||||||
 | 
					): React.ReactElement {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Paper elevation={2} style={{ padding: "10px" }}>
 | 
				
			||||||
 | 
					      <Typography
 | 
				
			||||||
 | 
					        variant="subtitle1"
 | 
				
			||||||
 | 
					        style={{ marginBottom: "10px", fontWeight: "bold" }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {p.label}
 | 
				
			||||||
 | 
					      </Typography>
 | 
				
			||||||
 | 
					      {p.children}
 | 
				
			||||||
 | 
					    </Paper>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								virtweb_frontend/src/widgets/VirtWebRouteContainer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								virtweb_frontend/src/widgets/VirtWebRouteContainer.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { Typography } from "@mui/material";
 | 
				
			||||||
 | 
					import { PropsWithChildren } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function VirtWebRouteContainer(
 | 
				
			||||||
 | 
					  p: {
 | 
				
			||||||
 | 
					    label: string;
 | 
				
			||||||
 | 
					  } & PropsWithChildren
 | 
				
			||||||
 | 
					): React.ReactElement {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div style={{ margin: "50px" }}>
 | 
				
			||||||
 | 
					      <Typography variant="h4" style={{ marginBottom: "20px" }}>
 | 
				
			||||||
 | 
					        {p.label}
 | 
				
			||||||
 | 
					      </Typography>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {p.children}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user