From f3ba7f03b65877c8bf76304de14ffa9a72ded09b Mon Sep 17 00:00:00 2001
From: Pijar <pierre.jarriges@tutanota.com>
Date: Tue, 24 May 2022 15:08:49 +0200
Subject: [PATCH] create update delete / todo: tests- autodelete

---
 Cargo.lock                                    |  17 +--
 Cargo.toml                                    |   1 +
 admin-frontend/src/article.js                 |   3 +
 .../src/components/create-article-form.js     |  13 +++
 mongo/scripts/migration.js                    |   2 +-
 public/games/index.html                       |  25 ++--
 src/env.rs                                    |   3 +
 src/model/article.rs                          |  16 +++
 src/service/articles.rs                       | 108 ++++++++++++++++--
 src/static_view.rs                            |  24 +++-
 10 files changed, 181 insertions(+), 31 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 873731d..c51b74f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -342,9 +342,9 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.15"
+version = "0.7.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
 dependencies = [
  "memchr",
 ]
@@ -1280,6 +1280,7 @@ dependencies = [
  "futures",
  "magic-crypt",
  "rand 0.8.4",
+ "regex",
  "rustls 0.18.1",
  "serde",
  "serde_json",
@@ -1381,9 +1382,9 @@ dependencies = [
 
 [[package]]
 name = "memchr"
-version = "2.3.4"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
 [[package]]
 name = "mime"
@@ -1872,9 +1873,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.4.6"
+version = "1.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
+checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1883,9 +1884,9 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.25"
+version = "0.6.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
 
 [[package]]
 name = "reqwest"
diff --git a/Cargo.toml b/Cargo.toml
index 263edf0..5a85d62 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,4 +22,5 @@ chrono = "0.4"
 rand = "0.8"
 dotenv = "0.15"
 time = "0.2.7"
+regex = "1.5"
 tokio = { version = "0.2", features = ["full"] }
diff --git a/admin-frontend/src/article.js b/admin-frontend/src/article.js
index c583c3d..c2cf4fb 100644
--- a/admin-frontend/src/article.js
+++ b/admin-frontend/src/article.js
@@ -13,6 +13,9 @@ class Article {
             this.body = "";
             this.locale = "";
             this.display_priority_index = 1;
+            this.metadata = {
+                description: "",
+            };
         }
     }
 
diff --git a/admin-frontend/src/components/create-article-form.js b/admin-frontend/src/components/create-article-form.js
index 7f5b5b2..b728b5a 100644
--- a/admin-frontend/src/components/create-article-form.js
+++ b/admin-frontend/src/components/create-article-form.js
@@ -19,6 +19,13 @@ class CreateArticleForm {
         this.refresh();
     }
 
+    handle_change_metadata(field, e) {
+        const metadata = Object.assign(this.state.output.metadata, {
+            [field]: e.target.value,
+        });
+        this.state.output.metadata = metadata;
+    }
+
     handle_text_input(field, e) {
         this.state.output[field] = e.target.value;
     }
@@ -347,6 +354,12 @@ class CreateArticleForm {
                     placeholder: "Article body",
                     oninput: this.handle_text_input.bind(this, "body")
                 },
+                {
+                    tag: "input", type: "text",
+                    value: this.state.output.metadata.description,
+                    placeholder: "description",
+                    oninput: this.handle_change_metadata.bind(this, "description"),
+                },
                 this.render_details_inputs(),
                 this.render_images_inputs(),
                 { tag: "input", type: "submit" }
diff --git a/mongo/scripts/migration.js b/mongo/scripts/migration.js
index 33329ed..5e8e966 100644
--- a/mongo/scripts/migration.js
+++ b/mongo/scripts/migration.js
@@ -8,6 +8,6 @@ db.auth(adminname, adminpwd);
 articles = db.getCollection("articles");
 
 articles.update({},
-    { $set: { "display_priority_index": NumberInt(1) } },
+    { $set: { "metadata": { "description": "" } } },
     { upsert: false, multi: true }
 );
\ No newline at end of file
diff --git a/public/games/index.html b/public/games/index.html
index ebb6c41..4b82f26 100644
--- a/public/games/index.html
+++ b/public/games/index.html
@@ -4,20 +4,22 @@
 <head>
     <meta charset="utf-8" />
     <title>Kuadrado Software | Jeux</title>
-    <meta name="description" content="Création de jeux vidéos indépendants. Jeux web, PC et projets en cours de développement"/>
+    <meta name="description"
+        content="Création de jeux vidéos indépendants. Jeux web, PC et projets en cours de développement" />
     <meta name="author" content="Kuadrado Software" />
-    <meta name="image" content="https://kuadrado-software.fr/assets/images/game_controller.png"/>
+    <meta name="image" content="https://kuadrado-software.fr/assets/images/game_controller.png" />
 
     <!-- Open Graph Protocol meta data -->
-    <meta property="og:title" content="Kuadrado Software | Jeux"/>
-    <meta property="og:description" content="Création de jeux vidéos indépendants. Jeux web, PC et projets en cours de développement"/>
+    <meta property="og:title" content="Kuadrado Software | Jeux" />
+    <meta property="og:description"
+        content="Création de jeux vidéos indépendants. Jeux web, PC et projets en cours de développement" />
     <meta property="og:type" content="website" />
-    <meta property="og:url" content="https://kuadrado-software.fr/games"/>
-    <meta property="og:image" content="https://kuadrado-software.fr/assets/images/game_controller.png"/>
-<meta property="og:image" content="https://kuadrado-software.fr/assets/images/game_studio_banner.png"/>
-<meta property="og:image" content="https://kuadrado-software.fr/assets/images/popularization_banner.png"/>
-    <meta property="twitter:image" content="https://kuadrado-software.fr/assets/images/game_controller.png"/>
-    <meta property="og:locale" content="fr_FR"/>
+    <meta property="og:url" content="https://kuadrado-software.fr/games" />
+    <meta property="og:image" content="https://kuadrado-software.fr/assets/images/game_controller.png" />
+    <meta property="og:image" content="https://kuadrado-software.fr/assets/images/game_studio_banner.png" />
+    <meta property="og:image" content="https://kuadrado-software.fr/assets/images/popularization_banner.png" />
+    <meta property="twitter:image" content="https://kuadrado-software.fr/assets/images/game_controller.png" />
+    <meta property="og:locale" content="fr_FR" />
     <meta property="og:site_name" content="Kuadrado Software" />
 
     <!-- English translation not ready yet -->
@@ -26,7 +28,8 @@
     <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
     <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
     <link href="/style/style.css" rel="stylesheet" />
-    <script type="application/ld+json">{"@context":"https://schema.org","type":"WebPage","description":"Création de jeux vidéos indépendants. Jeux web, PC et projets en cours de développement","image":["https://kuadrado-software.fr/assets/images/game_controller.svg","https://kuadrado-software.fr/assets/images/game_controller.png"],"keywords":"gamedev, pixelart, jeux vidéo, création, video games, indépendants, indie gamedev","name":"Kuadrado Software - Jeux","url":"https://kuadrado-software.fr/games"}</script>
+    <script
+        type="application/ld+json">{"@context":"https://schema.org","type":"WebPage","description":"Création de jeux vidéos indépendants. Jeux web, PC et projets en cours de développement","image":["https://kuadrado-software.fr/assets/images/game_controller.svg","https://kuadrado-software.fr/assets/images/game_controller.png"],"keywords":"gamedev, pixelart, jeux vidéo, création, video games, indépendants, indie gamedev","name":"Kuadrado Software - Jeux","url":"https://kuadrado-software.fr/games"}</script>
 </head>
 <!-- The vocab attribute defines the standard vocabulary used for RDFa standard.
     The DOM may contain properties such as "typeof" and "property" accordinly to the schema.org vocabulary -->
diff --git a/src/env.rs b/src/env.rs
index c0b0b99..cb98d28 100644
--- a/src/env.rs
+++ b/src/env.rs
@@ -9,6 +9,7 @@ pub struct Env {
     pub db_name: String,
     pub db_port: String,
     pub server_host: String,
+    pub server_protocol: String,
     pub crypt_key: String,
     pub default_admin_username: String,
     pub default_admin_password: String,
@@ -50,6 +51,7 @@ impl Env {
             db_name: env::var("DATABASE_NAME").expect("DATABASE_NAME is not defined."),
             db_port: env::var("DB_PORT").expect("DB_PORT is not defined."),
             server_host: env::var("SERVER_HOST").expect("SERVER_HOST is not defined"),
+            server_protocol: env::var("SERVER_PROTOCOL").expect("SERVER_PROTOCOL is not defined"),
             crypt_key: env::var("CRYPT_KEY").expect("CRYPT_KEY is not defined."),
             default_admin_username: env::var("DEFAULT_ADMIN_USERNAME")
                 .expect("DEFAULT_ADMIN_USERNAME is not defined"),
@@ -72,6 +74,7 @@ impl Env {
             db_name: env::var("DATABASE_NAME").expect("DATABASE_NAME is not defined."),
             db_port: env::var("DB_PORT").expect("DB_PORT is not defined."),
             server_host: env::var("SERVER_HOST").expect("SERVER_HOST is not defined"),
+            server_protocol: env::var("SERVER_PROTOCOL").expect("SERVER_PROTOCOL is not defined"),
             crypt_key: env::var("CRYPT_KEY").expect("CRYPT_KEY is not defined."),
             default_admin_username: env::var("DEFAULT_ADMIN_USERNAME")
                 .expect("DEFAULT_ADMIN_USERNAME is not defined"),
diff --git a/src/model/article.rs b/src/model/article.rs
index f080e53..0b45eb0 100644
--- a/src/model/article.rs
+++ b/src/model/article.rs
@@ -1,6 +1,7 @@
 #[cfg(test)]
 use chrono::Utc;
 use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
 use wither::{
     bson::{doc, oid::ObjectId, DateTime},
     prelude::Model,
@@ -12,6 +13,14 @@ pub struct ArticleDetail {
     pub value: String,
 }
 
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct ArticleMetadata {
+    pub description: String,
+    pub view_uri: Option<String>,
+    pub static_resource_path: Option<PathBuf>,
+    pub slug: Option<String>,
+}
+
 #[derive(Debug, Serialize, Deserialize, Model, Clone)]
 #[model(index(keys = r#"doc!{"title": 1}"#, options = r#"doc!{"unique": true}"#))]
 pub struct Article {
@@ -26,6 +35,7 @@ pub struct Article {
     pub category: String,
     pub locale: String,
     pub display_priority_index: i8,
+    pub metadata: ArticleMetadata,
 }
 
 impl Article {
@@ -51,6 +61,12 @@ impl Article {
             category: "testing".to_string(),
             locale: "fr".to_string(),
             display_priority_index: 1,
+            metadata: ArticleMetadata {
+                description: "A test article".to_string(),
+                view_uri: None,
+                static_resource_path: None,
+                slug: None,
+            },
         }
     }
 }
diff --git a/src/service/articles.rs b/src/service/articles.rs
index c38caf1..7fa896c 100644
--- a/src/service/articles.rs
+++ b/src/service/articles.rs
@@ -1,5 +1,7 @@
 use crate::{
-    middleware::AuthenticatedAdminMiddleware, model::Article, static_view::create_static_view,
+    middleware::AuthenticatedAdminMiddleware,
+    model::Article,
+    static_view::{create_static_view, delete_static_view},
     AppState,
 };
 use actix_web::{
@@ -9,6 +11,7 @@ use actix_web::{
 };
 use chrono::Utc;
 use futures::stream::StreamExt;
+use regex::Regex;
 use serde::{Deserialize, Serialize};
 use wither::{
     bson::{doc, oid::ObjectId, Bson, DateTime},
@@ -25,6 +28,37 @@ fn get_collection(app_state: &AppState) -> Collection<Article> {
     app_state.db.collection_with_type::<Article>("articles")
 }
 
+fn slugify(s: &String) -> String {
+    let s = s.to_lowercase();
+    // Delete every character that is not a letter a number or a space
+    let re = Regex::new(r"[^a-z\d\s]").unwrap();
+    let slug = re.replace_all(&s, "");
+
+    let slug = slug.trim().to_string();
+    // Deduplicate whitespaces and replace them by hyhens
+    let re = Regex::new(r"[\s]+").unwrap();
+    let slug = re.replace_all(&slug, "-");
+
+    slug.to_string()
+}
+
+fn create_article_static_view_metadata(app_state: &AppState, art: &mut Article) {
+    let article_slug = slugify(&art.title);
+    art.metadata.slug = Some(article_slug.to_owned());
+    art.metadata.view_uri = Some(format!(
+        "{}://{}/{}/{}/",
+        &app_state.env.server_protocol, &app_state.env.server_host, &art.category, &article_slug,
+    ));
+    art.metadata.static_resource_path = Some(
+        app_state
+            .env
+            .public_dir
+            .join(&art.category)
+            .join("view")
+            .join(&article_slug),
+    );
+}
+
 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) => {
@@ -33,6 +67,20 @@ async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<()
             }
 
             let art = art.unwrap();
+
+            if art.metadata.view_uri.is_none() {
+                return Err(format!("Article {} doesn't have view uri", art.title));
+            }
+
+            let art_img_def = String::new();
+            let mut art_image_uri = art.images.iter().next().unwrap_or(&art_img_def).to_owned();
+
+            if !art_image_uri.is_empty() {
+                art_image_uri = format!("/assets/images/{}", art_image_uri);
+            }
+
+            let slug = art.metadata.slug.unwrap();
+
             let html = format!(
                 "
 <html lang='{}'>
@@ -40,20 +88,30 @@ async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<()
     <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='author' content='Kuadrado Software' />
+    <meta name='image' content='{}'/>
     <meta name='description' content='{}'>
+    <link rel='icon' type='image/svg+xml' href='/favicon.svg' />
     <title>{}</title>
+    <link href='/style/style.css' rel='stylesheet' />
 </head>
 <body>
     <div>{}<div>
 </body>
+<script type='text/javascript' src='/games/view.js'></script>
 </html>
             ",
-                art.locale, art.subtitle, art.title, art.body,
+                art.locale, art_image_uri, art.metadata.description, art.title, art.body,
             );
 
-            create_static_view(&app_state, art.category, art.title, html)
+            create_static_view(&app_state, art.category, slug, html)
+        }
+        Err(e) => {
+            return Err(format!(
+                "Database error searching for article with id {} : {}",
+                id, e
+            ))
         }
-        Err(e) => return Err(format!("ERR {}", e)),
     }
 }
 
@@ -71,6 +129,8 @@ pub async fn post_article(
     let mut article_data = article_data.into_inner();
     article_data.date = Some(DateTime(Utc::now()));
 
+    create_article_static_view_metadata(&app_state, &mut article_data);
+
     match get_collection(&app_state)
         .insert_one(article_data, None)
         .await
@@ -118,11 +178,27 @@ pub async fn update_article(
     let mut article_data = article_data.into_inner();
     article_data.date = Some(DateTime(Utc::now()));
 
+    if let Err(e) = delete_static_view(&article_data.metadata.static_resource_path) {
+        return HttpResponse::InternalServerError()
+            .body(format!("Error removing previous static view : {}", e));
+    };
+
+    create_article_static_view_metadata(&app_state, &mut article_data);
+
     match get_collection(&app_state)
         .find_one_and_replace(doc! {"_id": &article_id}, article_data, None)
         .await
     {
-        Ok(res) => HttpResponse::Ok().json(res.unwrap()),
+        Ok(res) => match res {
+            Some(art) => {
+                if let Err(e) = generate_article_view(&app_state, &art.clone().id.unwrap()).await {
+                    return HttpResponse::InternalServerError()
+                        .body(format!("Error generating article static view {}", e));
+                };
+                HttpResponse::Ok().json(art)
+            }
+            None => HttpResponse::InternalServerError().finish(),
+        },
         Err(_) => HttpResponse::InternalServerError().finish(),
     }
 }
@@ -146,11 +222,23 @@ pub async fn delete_article(
         }
     };
 
-    match get_collection(&app_state)
-        .find_one_and_delete(doc! {"_id": &article_id}, None)
-        .await
-    {
-        Ok(_) => HttpResponse::Accepted().body("Article was deleted"),
+    let articles = get_collection(&app_state);
+    match articles.find_one(doc! {"_id": &article_id}, None).await {
+        Ok(article) => match article {
+            Some(art) => {
+                if let Err(e) = delete_static_view(&art.metadata.static_resource_path) {
+                    return HttpResponse::InternalServerError()
+                        .body(format!("Error removing article static view {}", e));
+                };
+
+                match articles.delete_one(doc! {"_id": &article_id}, None).await {
+                    Ok(_) => HttpResponse::Accepted().body("Article was deleted"),
+                    Err(e) => HttpResponse::InternalServerError()
+                        .body(format!("Error deleting article  {}", e)),
+                }
+            }
+            None => HttpResponse::NotFound().body("Article was not found"),
+        },
         Err(e) => HttpResponse::InternalServerError().body(&format!("{:?}", e)),
     }
 }
diff --git a/src/static_view.rs b/src/static_view.rs
index 75cc486..e2e3294 100644
--- a/src/static_view.rs
+++ b/src/static_view.rs
@@ -1,6 +1,7 @@
 use crate::app_state::AppState;
-use std::fs::{create_dir, create_dir_all, File};
+use std::fs::{create_dir, create_dir_all, remove_dir, remove_file, File};
 use std::io::Write;
+use std::path::PathBuf;
 
 pub fn create_static_view(
     app_state: &AppState,
@@ -38,3 +39,24 @@ pub fn create_static_view(
         Err(e) => Err(format!("Error creating {:?} : {}", f_path, e)),
     }
 }
+
+pub fn delete_static_view(path: &Option<PathBuf>) -> Result<(), String> {
+    if let Some(path) = path {
+        if path.exists() {
+            if let Err(e) = remove_file(path) {
+                return Err(format!("Error deleting static view at {:?} : {}", path, e));
+            };
+
+            let parent = path.parent().unwrap();
+
+            if let Err(e) = remove_dir(parent) {
+                return Err(format!(
+                    "Error deleting static view directory at {:?} : {}",
+                    parent, e
+                ));
+            }
+        }
+    }
+
+    Ok(())
+}
-- 
GitLab