Compare commits
6 Commits
6cd8a2edc1
...
bg_tasks
| Author | SHA1 | Date | |
|---|---|---|---|
|
aed6093a50
|
|||
|
9a9f92ed95
|
|||
|
0978b6c702
|
|||
|
b950e27e05
|
|||
|
da84cf7ee0
|
|||
|
88c90cd4c2
|
58
Cargo.lock
generated
58
Cargo.lock
generated
@@ -1364,6 +1364,7 @@ dependencies = [
|
||||
name = "peazyweb-backend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"rocket",
|
||||
"rocket_cors",
|
||||
"tokio",
|
||||
@@ -1383,6 +1384,7 @@ dependencies = [
|
||||
"web-sys",
|
||||
"yew",
|
||||
"yew-router",
|
||||
"yewlish-checkbox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2719,6 +2721,62 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yewlish-attr-passer"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "081aa452cbf823dca91349d0396b3e316126116fe15618944ed0c95dc99e0dee"
|
||||
dependencies = [
|
||||
"log",
|
||||
"yew",
|
||||
"yewlish-synchi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yewlish-checkbox"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "459ff19b11ddc68f67915cce83668d026120ccda15f993326b0668ab8f4e8137"
|
||||
dependencies = [
|
||||
"web-sys",
|
||||
"yew",
|
||||
"yewlish-attr-passer",
|
||||
"yewlish-presence",
|
||||
"yewlish-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yewlish-presence"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b41a691278fb1b2c59b1ef566d61bf8c398cdd520e13d0458f6f03f3c8c0f350"
|
||||
dependencies = [
|
||||
"log",
|
||||
"web-sys",
|
||||
"yew",
|
||||
"yewlish-attr-passer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yewlish-synchi"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8b53756f149fbc66294a7d13939da6fca78f55a98892f0ad5769ac3d61a6bf8"
|
||||
dependencies = [
|
||||
"yew",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yewlish-utils"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f951bd01648f6429f9ea2d78c4b60677952eac2e6f2a85af241bd79b7a19959c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"web-sys",
|
||||
"yew",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.5.0"
|
||||
rocket = { version = "0.5.1", features = ["json", "serde_json"] }
|
||||
rocket_cors = "0.6.0"
|
||||
tokio = { version = "1.44.1", features = ["fs", "process"] }
|
||||
|
||||
61
backend/src/common.rs
Normal file
61
backend/src/common.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::{env, path::Path};
|
||||
|
||||
use rocket::response::Responder;
|
||||
|
||||
#[derive(Responder)]
|
||||
pub(crate) enum GenerationError {
|
||||
#[response(status = 500, content_type = "json")]
|
||||
InternalError(String),
|
||||
#[response(status = 404, content_type = "json")]
|
||||
DirectoryNotFoundError(String),
|
||||
}
|
||||
|
||||
pub(crate) fn get_base_directory() -> String {
|
||||
match env::var("GENERATION_BASE_DIRECTORY") {
|
||||
Ok(directory) => directory,
|
||||
Err(_) => "base/".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn check_is_valid_directory(
|
||||
directory: impl AsRef<Path> + Clone,
|
||||
) -> Result<(), GenerationError> {
|
||||
match tokio::fs::metadata(directory.clone()).await {
|
||||
Ok(metadata) => {
|
||||
if !metadata.is_dir() {
|
||||
return Err(GenerationError::DirectoryNotFoundError(
|
||||
"The specified directory is not valid".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(GenerationError::DirectoryNotFoundError(
|
||||
"The specified directory is not exists".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the directory contains a 'vars.bat' and 'template.ovpn' files
|
||||
const REQUIRED_FILES: [&str; 2] = ["vars.bat", "template.ovpn"];
|
||||
for file_name in REQUIRED_FILES.iter() {
|
||||
let file_path = directory.as_ref().join(file_name);
|
||||
match tokio::fs::metadata(&file_path).await {
|
||||
Ok(metadata) => {
|
||||
if !metadata.is_file() {
|
||||
return Err(GenerationError::DirectoryNotFoundError(format!(
|
||||
"The specified directory is not valid, invalid file: {}",
|
||||
file_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(GenerationError::DirectoryNotFoundError(format!(
|
||||
"The specified directory is not valid, missing file: {}",
|
||||
file_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,79 +1,33 @@
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::fs::TempFile;
|
||||
use rocket::http::Method;
|
||||
use rocket::http::Status;
|
||||
use rocket::response::Redirect;
|
||||
use rocket::response::{Responder, status};
|
||||
use rocket::serde::{Deserialize, json::Json};
|
||||
use rocket::uri;
|
||||
use rocket::{self, get, launch, post, routes};
|
||||
use rocket::{self, get, launch, post, put, routes};
|
||||
use rocket_cors::{AllowedOrigins, CorsOptions};
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::common::{get_base_directory, check_is_valid_directory, GenerationError};
|
||||
|
||||
pub(crate) mod tasks;
|
||||
pub(crate) mod common;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct GenerationRequest<'r> {
|
||||
directory: &'r str,
|
||||
common_name: &'r str,
|
||||
}
|
||||
|
||||
#[derive(Responder)]
|
||||
enum GenerationError {
|
||||
#[response(status = 500, content_type = "json")]
|
||||
InternalError(String),
|
||||
#[response(status = 404, content_type = "json")]
|
||||
DirectoryNotFoundError(String),
|
||||
}
|
||||
|
||||
fn get_base_directory() -> String {
|
||||
match env::var("GENERATION_BASE_DIRECTORY") {
|
||||
Ok(directory) => directory,
|
||||
Err(_) => "base/".into(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_is_valid_directory(
|
||||
directory: impl AsRef<Path> + Clone,
|
||||
) -> Result<(), GenerationError> {
|
||||
match tokio::fs::metadata(directory.clone()).await {
|
||||
Ok(metadata) => {
|
||||
if !metadata.is_dir() {
|
||||
return Err(GenerationError::DirectoryNotFoundError(
|
||||
"The specified directory is not valid".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(GenerationError::DirectoryNotFoundError(
|
||||
"The specified directory is not exists".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the directory contains a 'vars.bat' and 'template.ovpn' files
|
||||
const REQUIRED_FILES: [&str; 2] = ["vars.bat", "template.ovpn"];
|
||||
for file_name in REQUIRED_FILES.iter() {
|
||||
let file_path = directory.as_ref().join(file_name);
|
||||
match tokio::fs::metadata(&file_path).await {
|
||||
Ok(metadata) => {
|
||||
if !metadata.is_file() {
|
||||
return Err(GenerationError::DirectoryNotFoundError(format!(
|
||||
"The specified directory is not valid, invalid file: {}",
|
||||
file_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(GenerationError::DirectoryNotFoundError(format!(
|
||||
"The specified directory is not valid, missing file: {}",
|
||||
file_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[serde(default)]
|
||||
email: &'r str,
|
||||
#[serde(default)]
|
||||
days: u32,
|
||||
#[serde(default)]
|
||||
use_openssl: Option<bool>,
|
||||
}
|
||||
|
||||
#[get("/get")]
|
||||
@@ -90,12 +44,12 @@ async fn list_directories() -> Result<Json<Vec<String>>, status::Custom<String>>
|
||||
let mut directories = Vec::new();
|
||||
while let Ok(Some(entry)) = reader.next_entry().await {
|
||||
let path = entry.path();
|
||||
if check_is_valid_directory(&path).await.is_ok() {
|
||||
if let Some(name) = path.file_name() {
|
||||
if check_is_valid_directory(&path).await.is_ok()
|
||||
&& let Some(name) = path.file_name()
|
||||
{
|
||||
directories.push(name.to_str().unwrap().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(directories))
|
||||
}
|
||||
@@ -156,6 +110,37 @@ async fn get_file(directory: &str, file: &str) -> Result<NamedFile, status::NotF
|
||||
.map_err(|e| status::NotFound(e.to_string()))
|
||||
}
|
||||
|
||||
#[put("/put/<directory>/<name>", format = "plain", data = "<file>")]
|
||||
async fn put_file(directory: &str, name: &str, mut file: TempFile<'_>) -> status::Custom<String> {
|
||||
let dir = Path::new(&get_base_directory()).join(directory);
|
||||
if check_is_valid_directory(&dir).await.is_err() {
|
||||
return status::Custom(
|
||||
Status::BadRequest,
|
||||
"The specified directory is not valid".into(),
|
||||
);
|
||||
}
|
||||
|
||||
let dir = dir.join("config");
|
||||
let path = dir.join(name);
|
||||
|
||||
// check if the file exists
|
||||
match tokio::fs::metadata(&path).await {
|
||||
Ok(meta) if meta.is_file() => {}
|
||||
_ => {
|
||||
return status::Custom(Status::NotFound, "The specified file is not found".into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(msg) = file.persist_to(&path).await {
|
||||
return status::Custom(
|
||||
Status::InternalServerError,
|
||||
format!("Failed to write file: {}", msg),
|
||||
);
|
||||
}
|
||||
|
||||
status::Custom(Status::NoContent, "".into())
|
||||
}
|
||||
|
||||
#[post("/generate", data = "<request>")]
|
||||
async fn generate(request: Json<GenerationRequest<'_>>) -> Result<NamedFile, GenerationError> {
|
||||
let dir = Path::new(&get_base_directory()).join(request.directory);
|
||||
@@ -163,12 +148,25 @@ async fn generate(request: Json<GenerationRequest<'_>>) -> Result<NamedFile, Gen
|
||||
|
||||
let generator_bin = env::var("GENERATOR_BIN").unwrap_or("peazyrsa".into());
|
||||
let mut cmd = tokio::process::Command::new(generator_bin);
|
||||
if env::var("USE_OPENSSL").unwrap_or("no".into()) == "yes" {
|
||||
|
||||
let mut use_openssl = env::var("USE_OPENSSL").unwrap_or("no".into()) == "yes";
|
||||
if let Some(req_use_openssl) = request.use_openssl {
|
||||
use_openssl = req_use_openssl;
|
||||
}
|
||||
if use_openssl {
|
||||
let openssl_bin = env::var("OPENSSL_BIN").unwrap_or("openssl".into());
|
||||
cmd.arg("--with-openssl").arg(openssl_bin);
|
||||
}
|
||||
cmd.arg("-d").arg(&dir).arg(request.common_name);
|
||||
|
||||
if !request.email.is_empty() {
|
||||
cmd.arg("--email").arg(request.email);
|
||||
}
|
||||
|
||||
if request.days > 0 {
|
||||
cmd.arg("--days").arg(request.days.to_string());
|
||||
}
|
||||
|
||||
// execute the command and check error code
|
||||
let status = cmd
|
||||
.status()
|
||||
@@ -225,7 +223,7 @@ fn rocket() -> _ {
|
||||
let cors = CorsOptions::default()
|
||||
.allowed_origins(AllowedOrigins::all())
|
||||
.allowed_methods(
|
||||
vec![Method::Get, Method::Post]
|
||||
vec![Method::Get, Method::Post, Method::Put]
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect(),
|
||||
@@ -235,7 +233,13 @@ fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.mount(
|
||||
"/api/v1",
|
||||
routes![list_directories, list_directory, get_file, generate],
|
||||
routes![
|
||||
list_directories,
|
||||
list_directory,
|
||||
get_file,
|
||||
put_file,
|
||||
generate
|
||||
],
|
||||
)
|
||||
.attach(cors.to_cors().unwrap())
|
||||
.mount("/", routes![index_redirect, frontend])
|
||||
|
||||
81
backend/src/tasks/memrepo.rs
Normal file
81
backend/src/tasks/memrepo.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::tasks::repo::{Task, TasksRepository};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
static ref LAST_ID: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
|
||||
pub(crate) static ref REPO: Arc<Mutex<MemoryTasksRepository>> =
|
||||
Arc::new(Mutex::new(MemoryTasksRepository::new())).clone();
|
||||
}
|
||||
|
||||
pub(crate) fn get_repo() -> Arc<Mutex<MemoryTasksRepository>> {
|
||||
REPO.clone()
|
||||
}
|
||||
|
||||
pub(crate) struct MemoryTasksRepository {
|
||||
pub tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
impl MemoryTasksRepository {
|
||||
pub(crate) fn new() -> Self {
|
||||
MemoryTasksRepository { tasks: Vec::new() }
|
||||
}
|
||||
|
||||
fn find_task_index(&self, task_id: u32) -> Option<usize> {
|
||||
for (index, task) in self.tasks.iter().enumerate() {
|
||||
if task.id == task_id {
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TasksRepository for MemoryTasksRepository {
|
||||
fn all_tasks(&self) -> Vec<Task> {
|
||||
self.tasks.clone()
|
||||
}
|
||||
|
||||
fn tasks_for(&self, directory_name: &str) -> Vec<Task> {
|
||||
self.tasks
|
||||
.iter()
|
||||
.filter(|task| task.directory_name == directory_name)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_task(
|
||||
&mut self,
|
||||
directory_name: String,
|
||||
config_names: Vec<String>,
|
||||
use_openssl: Option<bool>,
|
||||
) -> Task {
|
||||
let id = *LAST_ID.lock().unwrap() + 1;
|
||||
let task = Task::new(id, directory_name, config_names, use_openssl);
|
||||
self.tasks.push(task.clone());
|
||||
*LAST_ID.lock().unwrap() = id;
|
||||
|
||||
task
|
||||
}
|
||||
|
||||
fn update_task(&mut self, task: &Task) {
|
||||
if let Some(idx) = self.find_task_index(task.id) {
|
||||
self.tasks[idx] = task.clone();
|
||||
} else {
|
||||
self.tasks.push(task.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_task(&mut self, task_id: u32) {
|
||||
if let Some(idx) = self.find_task_index(task_id) {
|
||||
self.tasks.remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_task(&self, task_id: u32) -> Option<Task> {
|
||||
self.find_task_index(task_id)
|
||||
.map(|idx| self.tasks[idx].clone())
|
||||
}
|
||||
}
|
||||
3
backend/src/tasks/mod.rs
Normal file
3
backend/src/tasks/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub(crate) mod memrepo;
|
||||
pub(crate) mod repo;
|
||||
pub(crate) mod worker;
|
||||
84
backend/src/tasks/repo.rs
Normal file
84
backend/src/tasks/repo.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub(crate) enum TaskState {
|
||||
#[default]
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed {
|
||||
errors: Vec<String>,
|
||||
},
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Task {
|
||||
pub(crate) id: u32,
|
||||
pub(crate) directory_name: String,
|
||||
pub(crate) state: TaskState,
|
||||
pub(crate) config_names: Vec<String>,
|
||||
pub(crate) use_openssl: Option<bool>,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub(crate) fn new(
|
||||
id: u32,
|
||||
directory_name: String,
|
||||
config_names: Vec<String>,
|
||||
use_openssl: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
directory_name,
|
||||
state: TaskState::default(),
|
||||
config_names,
|
||||
use_openssl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait TasksRepository {
|
||||
fn all_tasks(&self) -> Vec<Task>;
|
||||
fn tasks_for(&self, directory_name: &str) -> Vec<Task>;
|
||||
fn add_task(
|
||||
&mut self,
|
||||
directory_name: String,
|
||||
config_names: Vec<String>,
|
||||
use_openssl: Option<bool>,
|
||||
) -> Task;
|
||||
fn update_task(&mut self, task: &Task);
|
||||
fn remove_task(&mut self, task_id: u32);
|
||||
fn get_task(&self, task_id: u32) -> Option<Task>;
|
||||
|
||||
fn take_next(&mut self) -> Option<Task> {
|
||||
if let Some(task) = self
|
||||
.all_tasks()
|
||||
.into_iter()
|
||||
.find(|t| t.state == TaskState::Pending)
|
||||
{
|
||||
let mut task = task.clone();
|
||||
task.state = TaskState::Running;
|
||||
self.update_task(&task);
|
||||
Some(task)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn complete_task(&mut self, task_id: u32) {
|
||||
if let Some(mut task) = self.get_task(task_id) {
|
||||
task.state = TaskState::Completed;
|
||||
self.update_task(&task);
|
||||
}
|
||||
}
|
||||
fn fail_task(&mut self, task_id: u32, errors: Vec<String>) {
|
||||
if let Some(mut task) = self.get_task(task_id) {
|
||||
task.state = TaskState::Failed { errors };
|
||||
self.update_task(&task);
|
||||
}
|
||||
}
|
||||
fn cancel_task(&mut self, task_id: u32) {
|
||||
if let Some(mut task) = self.get_task(task_id) {
|
||||
task.state = TaskState::Cancelled;
|
||||
self.update_task(&task);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
backend/src/tasks/worker.rs
Normal file
101
backend/src/tasks/worker.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::{env, path::Path};
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{common::{check_is_valid_directory, get_base_directory}, tasks::{self, memrepo::get_repo, repo::{Task, TasksRepository}}};
|
||||
|
||||
|
||||
pub(crate) async fn tasks_sender(tasks_tx: mpsc::Sender<Task>) {
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
while let Some(task) = get_repo().lock().unwrap().take_next() {
|
||||
if let Err(e) = tasks_tx.send(task).await {
|
||||
eprintln!("Error sending task: {}", e);
|
||||
};
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(0)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn tasks_updater(mut tasks_rx: mpsc::Receiver<Task>) {
|
||||
while let Some(task) = tasks_rx.recv().await {
|
||||
get_repo().lock().unwrap().update_task(&task);
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(0)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn task_runner(mut task: Task) -> Task {
|
||||
let dir = Path::new(&get_base_directory()).join(&task.directory_name);
|
||||
let dir_ok = check_is_valid_directory(dir.clone()).await;
|
||||
if dir_ok.is_err() {
|
||||
task.state = tasks::repo::TaskState::Failed{errors: task.config_names.clone()};
|
||||
return task;
|
||||
}
|
||||
|
||||
let generator_bin = env::var("GENERATOR_BIN").unwrap_or("peazyrsa".into());
|
||||
|
||||
|
||||
let mut use_openssl = env::var("USE_OPENSSL").unwrap_or("no".into()) == "yes";
|
||||
if let Some(req_use_openssl) = task.use_openssl {
|
||||
use_openssl = req_use_openssl;
|
||||
}
|
||||
let mut openssl_bin = "".to_string();
|
||||
if use_openssl {
|
||||
openssl_bin = env::var("OPENSSL_BIN").unwrap_or("openssl".into());
|
||||
}
|
||||
|
||||
let mut errors: Vec<String> = vec![];
|
||||
|
||||
for config_name in &task.config_names {
|
||||
let mut cmd = tokio::process::Command::new(&generator_bin);
|
||||
if use_openssl {
|
||||
cmd.arg("--with-openssl").arg(&openssl_bin);
|
||||
}
|
||||
cmd.arg("-d").arg(&dir).arg(&config_name);
|
||||
|
||||
|
||||
// execute the command and check error code
|
||||
match cmd.status().await {
|
||||
Ok(status) if status.success() => {
|
||||
|
||||
}
|
||||
_ => {
|
||||
errors.push(config_name.clone());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// check output file exists
|
||||
if tokio::fs::metadata(dir.join("config").join(format!("{}.ovpn", &config_name))).await.is_err() {
|
||||
errors.push(config_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
task.state = tasks::repo::TaskState::Completed;
|
||||
} else {
|
||||
task.state = tasks::repo::TaskState::Failed {
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
task
|
||||
|
||||
}
|
||||
|
||||
pub(crate) async fn worker(mut tasks_rx: mpsc::Receiver<Task>, tasks_tx: mpsc::Sender<Task>) {
|
||||
while let Some(task) = tasks_rx.recv().await {
|
||||
let task = tokio::spawn(async move { task_runner(task).await }).await.unwrap();
|
||||
if let Err(e) = tasks_tx.send(task).await {
|
||||
eprintln!("Error sending task back: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn start_worker() {
|
||||
let (tasks_tx_sender, tasks_rx_sender) = mpsc::channel(100);
|
||||
let (tasks_tx_updater, tasks_rx_updater) = mpsc::channel(100);
|
||||
tokio::spawn(tasks_sender(tasks_tx_sender));
|
||||
tokio::spawn(worker(tasks_rx_sender, tasks_tx_updater));
|
||||
tokio::spawn(tasks_updater(tasks_rx_updater));
|
||||
}
|
||||
@@ -14,3 +14,4 @@ wasm-bindgen-futures = "0.4.50"
|
||||
web-sys = { version = "0.3.77", features = ["console"] }
|
||||
yew = { version = "0.21.0", features = ["csr"] }
|
||||
yew-router = "0.18.0"
|
||||
yewlish-checkbox = "0.1.12"
|
||||
|
||||
29
frontend/src/components/input_value.rs
Normal file
29
frontend/src/components/input_value.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use yew::Properties;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
pub struct InputValueProps {
|
||||
pub id: AttrValue,
|
||||
#[prop_or("text".into())]
|
||||
pub _type: AttrValue,
|
||||
pub label: AttrValue,
|
||||
#[prop_or("".into())]
|
||||
pub placeholder: AttrValue,
|
||||
#[prop_or(false)]
|
||||
pub required: bool,
|
||||
pub oninput: Option<Callback<InputEvent>>,
|
||||
#[prop_or("block mb-2 text-sm font-medium text-gray-900 dark:text-white".into())]
|
||||
pub label_class: AttrValue,
|
||||
#[prop_or("bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white".into())]
|
||||
pub input_class: AttrValue,
|
||||
}
|
||||
|
||||
#[function_component(InputValue)]
|
||||
pub fn input_value(props: &InputValueProps) -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<label class={&props.label_class} for={&props.id}>{&props.label}</label>
|
||||
<input type={&props._type} id={&props.id} placeholder={&props.placeholder} class={&props.input_class} required={props.required} oninput={props.oninput.clone()} />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod alerts;
|
||||
pub mod input_value;
|
||||
pub mod navbar;
|
||||
pub mod pages;
|
||||
pub mod refresh_button;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub struct NavProps {
|
||||
pub fn navbar(props: &NavProps) -> Html {
|
||||
let classes_active = "block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0 dark:text-white md:dark:text-blue-500";
|
||||
let classes_inactive = "block py-2 px-3 text-gray-900 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent";
|
||||
let has_dir = props.currrent_dir.len() > 0;
|
||||
let has_dir = !props.currrent_dir.is_empty();
|
||||
let is_current_home = props.currrent_route == Route::Home;
|
||||
let is_current_dir = props.currrent_route == Route::Directories;
|
||||
|
||||
|
||||
@@ -2,16 +2,23 @@ use gloo::net::http::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use yew::prelude::*;
|
||||
use yewlish_checkbox::*;
|
||||
|
||||
use crate::{
|
||||
api::APIEndpoints,
|
||||
components::{alerts::ErrorAlert, pages::config::Config},
|
||||
components::{alerts::ErrorAlert, input_value::InputValue, pages::config::Config},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
struct GenerationRequest {
|
||||
directory: String,
|
||||
common_name: String,
|
||||
#[serde(default)]
|
||||
email: String,
|
||||
#[serde(default)]
|
||||
days: u32,
|
||||
#[serde(default)]
|
||||
use_openssl: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
@@ -23,6 +30,10 @@ pub struct GenerateProps {
|
||||
pub fn create_config(props: &GenerateProps) -> Html {
|
||||
let dir_name = props.dir.clone();
|
||||
let config_name = use_state(|| "".to_string());
|
||||
let email = use_state(|| "".to_string());
|
||||
let days: UseStateHandle<u32> = use_state(|| 0);
|
||||
let use_openssl: UseStateHandle<Option<bool>> = use_state(|| None);
|
||||
|
||||
let result = use_state(|| "".to_string());
|
||||
let error = use_state(|| "".to_string());
|
||||
let done = use_state(|| false);
|
||||
@@ -30,6 +41,10 @@ pub fn create_config(props: &GenerateProps) -> Html {
|
||||
let generate_config_request = {
|
||||
let dir_name = dir_name.clone();
|
||||
let config_name = config_name.clone();
|
||||
let email = email.clone();
|
||||
let days = days.clone();
|
||||
let use_openssl = use_openssl.clone();
|
||||
|
||||
let done = done.clone();
|
||||
let result = result.clone();
|
||||
let error = error.clone();
|
||||
@@ -37,6 +52,9 @@ pub fn create_config(props: &GenerateProps) -> Html {
|
||||
let request_body = GenerationRequest {
|
||||
directory: dir_name.clone().to_string(),
|
||||
common_name: config_name.clone().to_string(),
|
||||
email: email.clone().to_string(),
|
||||
days: *days.clone(),
|
||||
use_openssl: *use_openssl.clone(),
|
||||
};
|
||||
let json_payload = serde_json::to_string(&request_body).unwrap();
|
||||
let result = result.clone();
|
||||
@@ -69,21 +87,58 @@ pub fn create_config(props: &GenerateProps) -> Html {
|
||||
<>
|
||||
if !*done.clone() {
|
||||
<div class="grid place-items-center">
|
||||
<br/>
|
||||
<div class="w-full max-w-sm p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:p-6 md:p-8 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="w-full max-w-sm p-4 m-7 bg-white border border-gray-200 rounded-lg shadow-sm sm:p-6 md:p-8 dark:bg-gray-800 dark:border-gray-700">
|
||||
<form class="space-y-6" action="#">
|
||||
<h5 class="text-xl font-medium text-gray-900 dark:text-white">{format!("Создание конфигурации для {}:", props.dir.clone())}</h5>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="common-name">{"Имя:"}
|
||||
</label>
|
||||
<input type="text" id="common-name" placeholder="введите название" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required=true oninput={Callback::from({
|
||||
<InputValue id="common-name" label="Имя:" placeholder="введите название" required=true oninput={
|
||||
Callback::from({
|
||||
let config_name = config_name.clone();
|
||||
move |e: InputEvent| {
|
||||
let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
|
||||
config_name.set(input.value());
|
||||
}
|
||||
})} />
|
||||
})
|
||||
} />
|
||||
<InputValue id="email" label="Email:" placeholder="(необязательно)" oninput={
|
||||
Callback::from({
|
||||
let email = email.clone();
|
||||
move |e: InputEvent| {
|
||||
let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
|
||||
email.set(input.value());
|
||||
}
|
||||
})
|
||||
} />
|
||||
<InputValue id="days" label="Дней:" placeholder="(необязательно)" _type="number" oninput={
|
||||
Callback::from({
|
||||
let days = days.clone();
|
||||
move |e: InputEvent| {
|
||||
let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
|
||||
days.set(input.value().parse().unwrap_or_default());
|
||||
}
|
||||
})
|
||||
} />
|
||||
|
||||
<div>
|
||||
<label for={"use_openssl"} class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{"Использовать OpenSSL "}
|
||||
<Checkbox id="use_openssl" name="use_openssl" value="yes" default_checked={CheckedState::Indeterminate} on_checked_change={
|
||||
Callback::from(move |new_state: CheckedState| {
|
||||
let use_openssl = use_openssl.clone();
|
||||
use_openssl.set(Some(new_state == CheckedState::Checked));
|
||||
}) }>
|
||||
<CheckboxIndicator show_when={CheckedState::Checked}>
|
||||
{"✔"}
|
||||
</CheckboxIndicator>
|
||||
<CheckboxIndicator show_when={CheckedState::Indeterminate}>
|
||||
{"➖"}
|
||||
</CheckboxIndicator>
|
||||
<CheckboxIndicator show_when={CheckedState::Unchecked}>
|
||||
{"❌"}
|
||||
</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="button" onclick={generate_config_request.reform(|_| ())}
|
||||
class="text-white bg-gradient-to-r from-red-400 via-red-500 to-red-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 shadow-lg shadow-red-500/50 dark:shadow-lg dark:shadow-red-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2">
|
||||
{ "Создать" }
|
||||
|
||||
Reference in New Issue
Block a user