Compare commits

...

5 Commits

7 changed files with 215 additions and 16 deletions

57
Cargo.lock generated
View File

@@ -1383,6 +1383,7 @@ dependencies = [
"web-sys", "web-sys",
"yew", "yew",
"yew-router", "yew-router",
"yewlish-checkbox",
] ]
[[package]] [[package]]
@@ -2719,6 +2720,62 @@ dependencies = [
"syn 2.0.100", "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]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.5" version = "0.7.5"

View File

@@ -1,11 +1,12 @@
use rocket::fs::NamedFile; use rocket::fs::NamedFile;
use rocket::fs::TempFile;
use rocket::http::Method; use rocket::http::Method;
use rocket::http::Status; use rocket::http::Status;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::response::{Responder, status}; use rocket::response::{Responder, status};
use rocket::serde::{Deserialize, json::Json}; use rocket::serde::{Deserialize, json::Json};
use rocket::uri; use rocket::uri;
use rocket::{self, get, launch, post, routes}; use rocket::{self, get, launch, post, put, routes};
use rocket_cors::{AllowedOrigins, CorsOptions}; use rocket_cors::{AllowedOrigins, CorsOptions};
use std::env; use std::env;
use std::path::Path; use std::path::Path;
@@ -16,6 +17,12 @@ use std::path::PathBuf;
struct GenerationRequest<'r> { struct GenerationRequest<'r> {
directory: &'r str, directory: &'r str,
common_name: &'r str, common_name: &'r str,
#[serde(default)]
email: &'r str,
#[serde(default)]
days: u32,
#[serde(default)]
use_openssl: Option<bool>,
} }
#[derive(Responder)] #[derive(Responder)]
@@ -90,11 +97,10 @@ async fn list_directories() -> Result<Json<Vec<String>>, status::Custom<String>>
let mut directories = Vec::new(); let mut directories = Vec::new();
while let Ok(Some(entry)) = reader.next_entry().await { while let Ok(Some(entry)) = reader.next_entry().await {
let path = entry.path(); let path = entry.path();
if check_is_valid_directory(&path).await.is_ok() { if check_is_valid_directory(&path).await.is_ok()
if let Some(name) = path.file_name() { && let Some(name) = path.file_name() {
directories.push(name.to_str().unwrap().to_string()) directories.push(name.to_str().unwrap().to_string())
} }
}
} }
Ok(Json(directories)) Ok(Json(directories))
@@ -156,6 +162,37 @@ async fn get_file(directory: &str, file: &str) -> Result<NamedFile, status::NotF
.map_err(|e| status::NotFound(e.to_string())) .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>")] #[post("/generate", data = "<request>")]
async fn generate(request: Json<GenerationRequest<'_>>) -> Result<NamedFile, GenerationError> { async fn generate(request: Json<GenerationRequest<'_>>) -> Result<NamedFile, GenerationError> {
let dir = Path::new(&get_base_directory()).join(request.directory); let dir = Path::new(&get_base_directory()).join(request.directory);
@@ -163,12 +200,25 @@ async fn generate(request: Json<GenerationRequest<'_>>) -> Result<NamedFile, Gen
let generator_bin = env::var("GENERATOR_BIN").unwrap_or("peazyrsa".into()); let generator_bin = env::var("GENERATOR_BIN").unwrap_or("peazyrsa".into());
let mut cmd = tokio::process::Command::new(generator_bin); 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()); let openssl_bin = env::var("OPENSSL_BIN").unwrap_or("openssl".into());
cmd.arg("--with-openssl").arg(openssl_bin); cmd.arg("--with-openssl").arg(openssl_bin);
} }
cmd.arg("-d").arg(&dir).arg(request.common_name); 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 // execute the command and check error code
let status = cmd let status = cmd
.status() .status()
@@ -225,7 +275,7 @@ fn rocket() -> _ {
let cors = CorsOptions::default() let cors = CorsOptions::default()
.allowed_origins(AllowedOrigins::all()) .allowed_origins(AllowedOrigins::all())
.allowed_methods( .allowed_methods(
vec![Method::Get, Method::Post] vec![Method::Get, Method::Post, Method::Put]
.into_iter() .into_iter()
.map(From::from) .map(From::from)
.collect(), .collect(),
@@ -235,7 +285,13 @@ fn rocket() -> _ {
rocket::build() rocket::build()
.mount( .mount(
"/api/v1", "/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()) .attach(cors.to_cors().unwrap())
.mount("/", routes![index_redirect, frontend]) .mount("/", routes![index_redirect, frontend])

View File

@@ -14,3 +14,4 @@ wasm-bindgen-futures = "0.4.50"
web-sys = { version = "0.3.77", features = ["console"] } web-sys = { version = "0.3.77", features = ["console"] }
yew = { version = "0.21.0", features = ["csr"] } yew = { version = "0.21.0", features = ["csr"] }
yew-router = "0.18.0" yew-router = "0.18.0"
yewlish-checkbox = "0.1.12"

View 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>
}
}

View File

@@ -1,4 +1,5 @@
pub mod alerts; pub mod alerts;
pub mod input_value;
pub mod navbar; pub mod navbar;
pub mod pages; pub mod pages;
pub mod refresh_button; pub mod refresh_button;

View File

@@ -15,7 +15,7 @@ pub struct NavProps {
pub fn navbar(props: &NavProps) -> Html { 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_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 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_home = props.currrent_route == Route::Home;
let is_current_dir = props.currrent_route == Route::Directories; let is_current_dir = props.currrent_route == Route::Directories;

View File

@@ -2,16 +2,23 @@ use gloo::net::http::Request;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
use yew::prelude::*; use yew::prelude::*;
use yewlish_checkbox::*;
use crate::{ use crate::{
api::APIEndpoints, api::APIEndpoints,
components::{alerts::ErrorAlert, pages::config::Config}, components::{alerts::ErrorAlert, input_value::InputValue, pages::config::Config},
}; };
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
struct GenerationRequest { struct GenerationRequest {
directory: String, directory: String,
common_name: String, common_name: String,
#[serde(default)]
email: String,
#[serde(default)]
days: u32,
#[serde(default)]
use_openssl: Option<bool>,
} }
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
@@ -23,6 +30,10 @@ pub struct GenerateProps {
pub fn create_config(props: &GenerateProps) -> Html { pub fn create_config(props: &GenerateProps) -> Html {
let dir_name = props.dir.clone(); let dir_name = props.dir.clone();
let config_name = use_state(|| "".to_string()); 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 result = use_state(|| "".to_string());
let error = use_state(|| "".to_string()); let error = use_state(|| "".to_string());
let done = use_state(|| false); let done = use_state(|| false);
@@ -30,6 +41,10 @@ pub fn create_config(props: &GenerateProps) -> Html {
let generate_config_request = { let generate_config_request = {
let dir_name = dir_name.clone(); let dir_name = dir_name.clone();
let config_name = config_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 done = done.clone();
let result = result.clone(); let result = result.clone();
let error = error.clone(); let error = error.clone();
@@ -37,6 +52,9 @@ pub fn create_config(props: &GenerateProps) -> Html {
let request_body = GenerationRequest { let request_body = GenerationRequest {
directory: dir_name.clone().to_string(), directory: dir_name.clone().to_string(),
common_name: config_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 json_payload = serde_json::to_string(&request_body).unwrap();
let result = result.clone(); let result = result.clone();
@@ -69,21 +87,58 @@ pub fn create_config(props: &GenerateProps) -> Html {
<> <>
if !*done.clone() { if !*done.clone() {
<div class="grid place-items-center"> <div class="grid place-items-center">
<br/> <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">
<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">
<form class="space-y-6" action="#"> <form class="space-y-6" action="#">
<h5 class="text-xl font-medium text-gray-900 dark:text-white">{format!("Создание конфигурации для {}:", props.dir.clone())}</h5> <h5 class="text-xl font-medium text-gray-900 dark:text-white">{format!("Создание конфигурации для {}:", props.dir.clone())}</h5>
<div> <InputValue id="common-name" label="Имя:" placeholder="введите название" required=true oninput={
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="common-name">{"Имя:"} Callback::from({
</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({
let config_name = config_name.clone(); let config_name = config_name.clone();
move |e: InputEvent| { move |e: InputEvent| {
let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap(); let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
config_name.set(input.value()); 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> </div>
<button type="button" onclick={generate_config_request.reform(|_| ())} <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"> 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">
{ "Создать" } { "Создать" }