diff --git a/backend/src/main.rs b/backend/src/main.rs index 87b7992..d4fb2b8 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,9 +1,9 @@ use rocket::fs::NamedFile; +use rocket::http::Method; use rocket::http::Status; use rocket::response::{Responder, status}; use rocket::serde::{Deserialize, json::Json}; use rocket::{self, get, launch, post, routes}; -use rocket::http::Method; use rocket_cors::{AllowedOrigins, CorsOptions}; use std::env; use std::path::Path; @@ -206,18 +206,20 @@ async fn generate(request: Json<GenerationRequest<'_>>) -> Result<NamedFile, Gen #[launch] fn rocket() -> _ { - let cors = CorsOptions::default() - .allowed_origins(AllowedOrigins::all()) - .allowed_methods( - vec![Method::Get, Method::Post] - .into_iter() - .map(From::from) - .collect(), - ) - .allow_credentials(true); + let cors = CorsOptions::default() + .allowed_origins(AllowedOrigins::all()) + .allowed_methods( + vec![Method::Get, Method::Post] + .into_iter() + .map(From::from) + .collect(), + ) + .allow_credentials(true); - rocket::build().mount( - "/api/v1", - routes![index, list_directories, list_directory, get_file, generate], - ).attach(cors.to_cors().unwrap()) + rocket::build() + .mount( + "/api/v1", + routes![index, list_directories, list_directory, get_file, generate], + ) + .attach(cors.to_cors().unwrap()) } diff --git a/frontend/src/main.rs b/frontend/src/main.rs index 77f394f..41f9779 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -106,14 +106,12 @@ fn directories() -> Html { // Добавляем use_effect с пустым вектором зависимостей { let get_dirs = get_dirs.clone(); - use_effect_with((), - move |_| { - get_dirs.emit(()); - () - }, - ); + use_effect_with((), move |_| { + get_dirs.emit(()); + () + }); } - + html! { <div> <p><button @@ -185,8 +183,9 @@ fn configs(props: &ConfigsProps) -> Html { <p><button onclick={get_configs.reform(|_| ())} class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4"> - { "Refresh" } - </button></p> + { "Refresh" } </button> + <Link<Route> to={Route::Generate { dir_name: props.dir.clone().to_string() }}> <button class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4"> + { "Generate" } </button> </Link<Route>> </p> <h1>{ "Configs" }</h1> <h2 class="text-2xl font-bold text-gray-700 mb-2">{format!("Configs for {}:", props.dir.clone())}</h2> <ul class="list-disc pl-5"> @@ -249,30 +248,113 @@ fn config(props: &ConfigProps) -> Html { } html! { + <div class="w-full"> + <p><button + onclick={get_contents.reform(|_| ())} + class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4"> + { "Refresh" } + </button></p> + <p>{"Config "}<b>{props.file_name.clone()}</b>{" for "}<b>{props.dir.clone()}</b>{":"}</p> + <div class="mb-2 flex justify-between items-center"> + <p class="text-sm font-medium text-gray-900 dark:text-white">{"Config "}<b>{props.file_name.clone()}</b>{" for "}<b>{props.dir.clone()}</b>{":"}</p> + </div> + <div class="relative bg-gray-50 rounded-lg dark:bg-gray-700 p-4"> + <div class="overflow-scroll max-h-full"> + <pre><code id="code-block" class="text-sm text-gray-500 dark:text-gray-400 whitespace-pre">{&*contents}</code></pre> + </div> + <div class="absolute top-2 end-2 bg-gray-50 dark:bg-gray-700"> + <a href={format!("http://127.0.0.1:8000/api/v1/get/{}/{}", props.dir.clone(), props.file_name.clone())} download={props.file_name.clone()}><button type="button" class="text-white bg-gradient-to-br from-purple-600 to-blue-500 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2"> + <svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> + <path fill-rule="evenodd" d="M13 11.15V4a1 1 0 1 0-2 0v7.15L8.78 8.374a1 1 0 1 0-1.56 1.25l4 5a1 1 0 0 0 1.56 0l4-5a1 1 0 1 0-1.56-1.25L13 11.15Z" clip-rule="evenodd"/> + <path fill-rule="evenodd" d="M9.657 15.874 7.358 13H5a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2.358l-2.3 2.874a3 3 0 0 1-4.685 0ZM17 16a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17Z" clip-rule="evenodd"/> + </svg>{"Download"}</button></a> + </div> + </div> + </div> + } +} + +#[derive(Properties, PartialEq)] +pub struct GenerateProps { + pub dir: AttrValue, +} + +#[function_component(Generate)] +fn generate(props: &GenerateProps) -> Html { + let dir_name = props.dir.clone(); + let config_name = use_state(|| "".to_string()); + let result = use_state(|| "".to_string()); + let done = use_state(|| false); + + let generate_config = { + let dir_name = dir_name.clone(); + let config_name = config_name.clone(); + let done = done.clone(); + let result = result.clone(); + Callback::from(move |_| { + let request_body = GenerationRequest { + directory: dir_name.clone().to_string(), + common_name: config_name.clone().to_string(), + }; + let json_payload = serde_json::to_string(&request_body).unwrap(); + let url = "http://127.0.0.1:8000/api/v1/generate/"; + let result = result.clone(); + let done = done.clone(); + spawn_local(async move { + match Request::post(url) + .header("Content-Type", "application/json") + .body(json_payload) + .unwrap() + .send() + .await + { + Ok(response) if response.ok() => { + if let Ok(text) = response.text().await { + result.set(text); + done.set(true); + } + } + _ => { + gloo::console::log!("Failed to fetch response"); + } + } + }) + }) + }; + + html! { + <div> + if !*done.clone() { <div class="w-full"> <p><button - onclick={get_contents.reform(|_| ())} + onclick={generate_config.reform(|_| ())} class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4"> - { "Refresh" } + { "Generate" } </button></p> - <p>{"Config "}<b>{props.file_name.clone()}</b>{" for "}<b>{props.dir.clone()}</b>{":"}</p> <div class="mb-2 flex justify-between items-center"> - <p class="text-sm font-medium text-gray-900 dark:text-white">{"Config "}<b>{props.file_name.clone()}</b>{" for "}<b>{props.dir.clone()}</b>{":"}</p> + <p class="text-sm font-medium text-gray-900 dark:text-white">{"Enter the name of the config you want to generate:"}</p> + <form> + <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="common-name">{"Common Name:"}</label> + <input type="text" id="common-name" 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()); + } + })} class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500" /> + </form> </div> - <div class="relative bg-gray-50 rounded-lg dark:bg-gray-700 p-4"> - <div class="overflow-scroll max-h-full"> - <pre><code id="code-block" class="text-sm text-gray-500 dark:text-gray-400 whitespace-pre">{&*contents}</code></pre> - </div> - <div class="absolute top-2 end-2 bg-gray-50 dark:bg-gray-700"> - <a href={format!("http://127.0.0.1:8000/api/v1/get/{}/{}", props.dir.clone(), props.file_name.clone())} download={props.file_name.clone()}><button type="button" class="text-white bg-gradient-to-br from-purple-600 to-blue-500 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2"> - <svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> - <path fill-rule="evenodd" d="M13 11.15V4a1 1 0 1 0-2 0v7.15L8.78 8.374a1 1 0 1 0-1.56 1.25l4 5a1 1 0 0 0 1.56 0l4-5a1 1 0 1 0-1.56-1.25L13 11.15Z" clip-rule="evenodd"/> - <path fill-rule="evenodd" d="M9.657 15.874 7.358 13H5a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2.358l-2.3 2.874a3 3 0 0 1-4.685 0ZM17 16a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17Z" clip-rule="evenodd"/> - </svg>{"Download"}</button></a> - </div> </div> + } else { + <div> + <Link<Route> to={Route::Config{dir_name: dir_name.clone().to_string(), file_name: format!("{}.ovpn", config_name.clone().to_string())}}><button class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4">{ "Go to " } { config_name.clone().to_string() } </button></Link<Route>> + <p>{"Generated config for "}<b>{dir_name.clone()}</b>{" with name "}<b>{config_name.clone().to_string()}</b>{":"}</p> + <pre><code>{result.clone().to_string()}</code></pre> </div> + } + </div> + } } fn switch(routes: Route) -> Html { @@ -294,6 +376,12 @@ fn switch(routes: Route) -> Html { <Configs dir={dir_name.clone()} /> </div> }, + Route::Generate { dir_name } => html! { + <div> + <Navbar currrent_route={routes.clone()} currrent_dir={dir_name.clone()} /> + <Generate dir={dir_name.clone()} /> + </div> + }, Route::Config { dir_name, file_name,