diff --git a/.gitignore b/.gitignore index 8d7b400ad2178f45bdba1ead91052fb284e216f3..f500892940542a8edf87c12a7c2502885f175003 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ bundle.js node_modules target .env -public/**/*.js \ No newline at end of file +public/**/*.js +public/**/view/* \ No newline at end of file diff --git a/admin-frontend/src/article.js b/admin-frontend/src/article.js index 6ba6b37e8409d9ff8667e83f2e72264a9a9401c7..c583c3d94684bc947ebcf11577d71afd9d32cb75 100644 --- a/admin-frontend/src/article.js +++ b/admin-frontend/src/article.js @@ -12,6 +12,7 @@ class Article { this.images = []; this.body = ""; this.locale = ""; + this.display_priority_index = 1; } } diff --git a/admin-frontend/src/xhr.js b/admin-frontend/src/xhr.js index 32845c93ec1121fef9b79b74fc8c61c31191467c..5fc0148ff0d28e8c48bc08947afaaa348731a9db 100644 --- a/admin-frontend/src/xhr.js +++ b/admin-frontend/src/xhr.js @@ -11,25 +11,6 @@ async function fetch_article(article_id) { }) } -async function fetch_article_by_title(article_title) { - const form_data = new FormData(); - form_data.append("title", article_title); - - return new Promise((resolve, reject) => { - fetch(`/article-by-title/`, { - method: "POST", - body: new URLSearchParams(form_data), - }).then(async res => { - if (res.status >= 400 && res.status < 600) { - const text = await res.text(); - reject(text); - } else { - resolve(await res.json()); - } - }).catch(e => reject(e)) - }) -} - async function fetch_articles_by_category(category) { return new Promise((resolve, reject) => { fetch(`/articles/${category}`).then(async res => { @@ -126,7 +107,6 @@ async function fetch_all_articles() { module.exports = { fetch_article, - fetch_article_by_title, fetch_articles_by_category, fetch_post_article, fetch_update_article, diff --git a/dev.docker-compose.yml b/dev.docker-compose.yml index 0adb1e276001171a192bd4f17e6e0c97f250e09d..6847c9548ce1e5ea42ea4cee9a45e8a897519a6e 100644 --- a/dev.docker-compose.yml +++ b/dev.docker-compose.yml @@ -14,7 +14,7 @@ services: volumes: - ./src:/usr/src/kuadrado_server/src:ro - ./Cargo.toml:/usr/src/kuadrado_server/Cargo.toml:ro - - ./public:${RESOURCES_DIR}/public:ro + - ./public:${RESOURCES_DIR}/public - /etc/letsencrypt/:${RESOURCES_DIR}/certs:ro command: cargo run env_file: @@ -34,3 +34,7 @@ services: - ./mongo/scripts:/mongo-scripts ports: - "27017-27019:27017-27019" + deploy: + resources: + limits: + memory: 250M diff --git a/docker-compose.yml b/docker-compose.yml index 9bb27056ac8d5f468e91fe9bfdcb4d2d30ab6d39..0a9ac506a6a76e86d7518e219c487547e297cd87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,3 +31,7 @@ services: - ./mongo/scripts:/mongo-scripts ports: - "27017-27019:27017-27019" + deploy: + resources: + limits: + memory: 250M diff --git a/src/env.rs b/src/env.rs index cf5860ba3b3edac6a6ce52757cd4b1000b0c336a..c0b0b9900271ef347e75ee8c412f0cd25517627e 100644 --- a/src/env.rs +++ b/src/env.rs @@ -12,6 +12,7 @@ pub struct Env { pub crypt_key: String, pub default_admin_username: String, pub default_admin_password: String, + pub public_dir: std::path::PathBuf, } static RELEASE_MODES: [&str; 3] = ["debug", "test", "prod"]; @@ -54,6 +55,10 @@ impl Env { .expect("DEFAULT_ADMIN_USERNAME is not defined"), default_admin_password: env::var("DEFAULT_ADMIN_PASSWORD") .expect("DEFAULT_ADMIN_PASSWORD is not defined"), + public_dir: std::path::PathBuf::from( + env::var("RESOURCES_DIR").expect("RESOURCES_DIR is not defined"), + ) + .join("public"), } } @@ -72,6 +77,10 @@ impl Env { .expect("DEFAULT_ADMIN_USERNAME is not defined"), default_admin_password: env::var("DEFAULT_ADMIN_PASSWORD") .expect("DEFAULT_ADMIN_PASSWORD is not defined"), + public_dir: std::path::PathBuf::from( + env::var("RESOURCES_DIR").expect("RESOURCES_DIR is not defined"), + ) + .join("public"), } } } diff --git a/src/main.rs b/src/main.rs index d094fb032166982f6e59f38eacd0a8bc7f7ad11b..b8a1f8644140c25b34452c16e67d23ea8e6d1ce1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod middleware; mod model; mod service; mod standard_static_files; +mod static_view; mod tls; mod view; mod view_resource; @@ -34,10 +35,6 @@ async fn main() -> std::io::Result<()> { let server_port = env_var("SERVER_PORT").expect("SERVER_PORT is not defined."); let server_port_tls = env_var("SERVER_PORT_TLS").expect("SERVER_PORT_TLS is not defined."); - let public_dir = - std::path::PathBuf::from(env_var("RESOURCES_DIR").expect("RESOURCES_DIR is not defined")) - .join("public"); - let app_state = AppState::with_default_admin_user().await; HttpServer::new(move || { @@ -89,7 +86,6 @@ async fn main() -> std::io::Result<()> { .service(delete_article) .service(get_articles_by_category) .service(get_article) - .service(get_article_by_title) .service(get_all_articles) ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // STANDARD FILES /////////////////////////////////////////////////////////////////////////////////////////// @@ -102,18 +98,18 @@ async fn main() -> std::io::Result<()> { scope("/v") .service(Files::new( "/admin-panel/assets", - public_dir.join("views/admin-panel/assets"), + app_state.env.public_dir.join("views/admin-panel/assets"), )) .service(Files::new( "/admin-login/assets", - public_dir.join("views/admin-login/assets"), + app_state.env.public_dir.join("views/admin-login/assets"), )) // get_view will match any url to we put it at last .service(get_view), ) ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC WEBSITE ////////////////////////////////////////////////////////////////////////////////////////////// - .service(Files::new("/", &public_dir).index_file("index.html")) + .service(Files::new("/", &app_state.env.public_dir).index_file("index.html")) ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // empty 404 //////////////////////////////////////////////////////////////////////////////////////////////// .default_service(to(|| { diff --git a/src/service/articles.rs b/src/service/articles.rs index a1287cc0157e2d39f02899781c9297d112082314..c38caf10a5bffdc0701bf97478a3940792b70931 100644 --- a/src/service/articles.rs +++ b/src/service/articles.rs @@ -1,14 +1,17 @@ -use crate::{middleware::AuthenticatedAdminMiddleware, model::Article, AppState}; +use crate::{ + middleware::AuthenticatedAdminMiddleware, model::Article, static_view::create_static_view, + AppState, +}; use actix_web::{ delete, get, post, put, - web::{Data, Form, Json, Path}, + web::{Data, Json, Path}, HttpRequest, HttpResponse, Responder, }; use chrono::Utc; use futures::stream::StreamExt; use serde::{Deserialize, Serialize}; use wither::{ - bson::{doc, oid::ObjectId, DateTime}, + bson::{doc, oid::ObjectId, Bson, DateTime}, mongodb::Collection, prelude::Model, }; @@ -22,6 +25,38 @@ fn get_collection(app_state: &AppState) -> Collection<Article> { app_state.db.collection_with_type::<Article>("articles") } +async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<(), String> { + match Article::find_one(&app_state.db, doc! {"_id":&id}, None).await { + Ok(art) => { + if art.is_none() { + return Err(format!("Article with id {} was not found", id)); + } + + let art = art.unwrap(); + let html = format!( + " +<html lang='{}'> +<head> + <meta charset='UTF-8'> + <meta http-equiv='X-UA-Compatible' content='IE=edge'> + <meta name='viewport' content='width=device-width, initial-scale=1.0'> + <meta name='description' content='{}'> + <title>{}</title> +</head> +<body> + <div>{}<div> +</body> +</html> + ", + art.locale, art.subtitle, art.title, art.body, + ); + + create_static_view(&app_state, art.category, art.title, html) + } + Err(e) => return Err(format!("ERR {}", e)), + } +} + #[post("/post-article")] pub async fn post_article( app_state: Data<AppState>, @@ -40,7 +75,20 @@ pub async fn post_article( .insert_one(article_data, None) .await { - Ok(res) => HttpResponse::Created().json(res), + Ok(res) => { + match &res.inserted_id { + Bson::ObjectId(id) => { + if let Err(e) = generate_article_view(&app_state, id).await { + return HttpResponse::InternalServerError() + .body(format!("Error creating article page {:?}", e)); + } + } + _ => { + return HttpResponse::InternalServerError().body("Error creating article page") + } + } + HttpResponse::Created().json(res) + } Err(e) => { HttpResponse::InternalServerError().body(format!("Error inserting new article {:?}", e)) } @@ -159,24 +207,6 @@ pub async fn get_article(app_state: Data<AppState>, article_id: Path<String>) -> } } -#[post("/article-by-title")] -pub async fn get_article_by_title( - app_state: Data<AppState>, - form_data: Form<ArticleTitleFormData>, -) -> impl Responder { - let title = form_data.into_inner().title; - match Article::find_one(&app_state.db, doc! {"title":title}, None).await { - Ok(art) => { - if art.is_none() { - return HttpResponse::NotFound().body("Article was not found"); - } - - HttpResponse::Ok().json(art) - } - Err(e) => HttpResponse::InternalServerError().body(format!("Database error: {:#?}", e)), - } -} - #[get("/articles")] pub async fn get_all_articles(app_state: Data<AppState>) -> impl Responder { match get_collection(&app_state).find(None, None).await { @@ -221,7 +251,6 @@ mod test_articles { web::Bytes, App, }; - use wither::bson::Bson; async fn insert_test_article( app_state: &AppState, @@ -558,42 +587,6 @@ mod test_articles { assert_eq!(del_count, 1); } - #[tokio::test] - async fn test_get_article_by_title() { - dotenv::dotenv().ok(); - - let app_state = AppState::for_test().await; - - let mut app = test::init_service( - App::new() - .app_data(Data::new(app_state.clone())) - .service(get_article_by_title), - ) - .await; - - let article = Article::test_article(); - let (article_id, article_title) = insert_test_article(&app_state, article.clone()) - .await - .unwrap(); - - let req = test::TestRequest::with_uri("/article-by-title") - .header("Accept", "application/json") - .method(Method::POST) - .set_form(&ArticleTitleFormData { - title: article_title.to_owned(), - }) - .to_request(); - - let resp = test::call_service(&mut app, req).await; - - assert_eq!(resp.status(), StatusCode::OK); - let result: Article = test::read_body_json(resp).await; - assert_eq!(result.title, article_title); - - let del_count = delete_test_article(&app_state, &article_id).await.unwrap(); - assert_eq!(del_count, 1); - } - #[tokio::test] async fn test_get_articles_by_category() { dotenv::dotenv().ok(); diff --git a/src/static_view.rs b/src/static_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..75cc486764fb43c626cf301e81fd58c498ce3dc9 --- /dev/null +++ b/src/static_view.rs @@ -0,0 +1,40 @@ +use crate::app_state::AppState; +use std::fs::{create_dir, create_dir_all, File}; +use std::io::Write; + +pub fn create_static_view( + app_state: &AppState, + category: String, + filename: String, + html: String, +) -> Result<(), String> { + let view_path = app_state.env.public_dir.join(&category).join("view"); + + if !view_path.exists() { + if let Err(e) = create_dir(&view_path) { + return Err(format!("Couldn't create directory {:?}: {}", view_path, e)); + } + } + + let d_path = app_state + .env + .public_dir + .join(&category) + .join("view") + .join(&filename); + + if let Err(e) = create_dir_all(&d_path) { + return Err(format!("Error creating directory {:?} : {}", d_path, e)); + } + + let f_path = d_path.join("index.html"); + match File::create(&f_path) { + Ok(mut f) => { + if let Err(e) = f.write_all(html.as_bytes()) { + return Err(format!("Error writing to {:?} : {}", f_path, e)); + } + Ok(()) + } + Err(e) => Err(format!("Error creating {:?} : {}", f_path, e)), + } +} diff --git a/src/view_resource.rs b/src/view_resource.rs index 0678220a212fa65d82cfdcc0d9116dfe4ae2df95..a1ab07b6731806b9de116c81431aec315ded3ade 100644 --- a/src/view_resource.rs +++ b/src/view_resource.rs @@ -61,7 +61,7 @@ impl ViewResourceManager { let path: PathBuf = format!("{}/{}", dir_path.to_str().unwrap(), desc.index_file_name) .parse() .expect(&format!( - "Failed to pare resource index file path {:?}", + "Failed to parse resource index file path {:?}", desc.index_file_name ));