From f111b5c8bf73bf3d4ee12d14b0fee9186364ae6a Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Mon, 5 Dec 2022 17:40:38 +0530 Subject: [PATCH] feat: delete deployment from web UI closes: https://git.batsense.net/LibrePages/librepages/issues/13 --- src/ctx/api/v1/pages.rs | 16 +++ src/db.rs | 2 +- src/pages/dash/home.rs | 2 +- src/pages/dash/sites/add.rs | 47 ++++++- src/pages/dash/sites/delete.rs | 185 +++++++++++++++++++++++++ src/pages/dash/sites/mod.rs | 5 + src/pages/dash/sites/view.rs | 12 +- src/pages/routes.rs | 12 +- src/static_assets/mod.rs | 1 - src/tests.rs | 14 +- templates/pages/dash/sites/delete.html | 37 +++++ templates/pages/dash/sites/view.html | 3 +- 12 files changed, 323 insertions(+), 13 deletions(-) create mode 100644 src/pages/dash/sites/delete.rs create mode 100644 templates/pages/dash/sites/delete.html diff --git a/src/ctx/api/v1/pages.rs b/src/ctx/api/v1/pages.rs index 25fd312..d72fd44 100644 --- a/src/ctx/api/v1/pages.rs +++ b/src/ctx/api/v1/pages.rs @@ -16,6 +16,7 @@ */ use actix_web::web; use serde::{Deserialize, Serialize}; +use tokio::fs; use tokio::sync::oneshot; use uuid::Uuid; @@ -28,6 +29,7 @@ use crate::page_config; use crate::settings::Settings; use crate::subdomains::get_random_subdomain; use crate::utils::get_random; +use crate::utils::get_website_path; #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] /// Data required to add site @@ -96,4 +98,18 @@ impl Ctx { Err(ServiceError::WebsiteNotFound) } } + + pub async fn delete_site(&self, owner: String, site_id: Uuid) -> ServiceResult<()> { + if let Ok(db_site) = self.db.get_site_from_pub_id(site_id, owner).await { + let path = get_website_path(&self.settings, &db_site.hostname); + + fs::remove_dir_all(&path).await?; + self.db + .delete_site(&db_site.owner, &db_site.hostname) + .await?; + Ok(()) + } else { + Err(ServiceError::WebsiteNotFound) + } + } } diff --git a/src/db.rs b/src/db.rs index f7f1d17..d30327a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -301,7 +301,7 @@ impl Database { site_secret: site.site_secret, branch: site.branch, hostname: site.hostname, - owner: owner, + owner, repo_url: site.repo_url, pub_id, }; diff --git a/src/pages/dash/home.rs b/src/pages/dash/home.rs index 2cdda16..a2d57f6 100644 --- a/src/pages/dash/home.rs +++ b/src/pages/dash/home.rs @@ -47,7 +47,7 @@ pub struct TemplateSite { impl TemplateSite { pub fn new(site: Site, last_update: Option) -> Self { - let view = PAGES.dash.site.get_view(site.pub_id.clone()); + let view = PAGES.dash.site.get_view(site.pub_id); Self { site, last_update, diff --git a/src/pages/dash/sites/add.rs b/src/pages/dash/sites/add.rs index 46dab94..e7c334f 100644 --- a/src/pages/dash/sites/add.rs +++ b/src/pages/dash/sites/add.rs @@ -111,7 +111,9 @@ mod tests { use actix_web::http::StatusCode; use actix_web::test; + use crate::ctx::api::v1::auth::Password; use crate::ctx::ArcCtx; + use crate::errors::ServiceError; use crate::pages::dash::sites::add::TemplateAddSite; use crate::tests; use crate::*; @@ -160,7 +162,7 @@ mod tests { let event = event.pop().unwrap(); let headers = add_site.headers(); - let view_site = &PAGES.dash.site.get_view(site.pub_id.clone()); + let view_site = &PAGES.dash.site.get_view(site.pub_id); assert_eq!( headers.get(actix_web::http::header::LOCATION).unwrap(), view_site @@ -170,7 +172,7 @@ mod tests { let resp = get_request!(&app, view_site, cookies.clone()); assert_eq!(resp.status(), StatusCode::OK); let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); - assert!(res.contains(&site.site_secret)); + assert!(res.contains("****")); assert!(res.contains(&site.hostname)); assert!(res.contains(&site.repo_url)); assert!(res.contains(&site.branch)); @@ -178,6 +180,47 @@ mod tests { assert!(res.contains(&event.event_type.name)); assert!(res.contains(&event.id.to_string())); + let show_deploy_secret_route = format!("{view_site}?show_deploy_secret=true"); + let resp = get_request!(&app, &show_deploy_secret_route, cookies.clone()); + assert_eq!(resp.status(), StatusCode::OK); + let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); + assert!(res.contains(&site.site_secret)); + + // delete site + let delete_site = &PAGES.dash.site.get_delete(site.pub_id); + let resp = get_request!(&app, delete_site, cookies.clone()); + assert_eq!(resp.status(), StatusCode::OK); + let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); + assert!(res.contains(&site.hostname)); + + let msg = Password { + password: PASSWORD.into(), + }; + let resp = test::call_service( + &app, + post_request!(&msg, delete_site, FORM) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + // delete_request!(&app, delete_site, cookies.clone(), &msg, FORM); + assert_eq!(resp.status(), StatusCode::FOUND); + let headers = resp.headers(); + assert_eq!( + headers.get(actix_web::http::header::LOCATION).unwrap(), + PAGES.dash.home, + ); + + assert!(!utils::get_website_path(&ctx.settings, &site.hostname).exists()); + assert_eq!( + ctx.db + .get_site_from_pub_id(site.pub_id, NAME.into()) + .await + .err(), + Some(ServiceError::WebsiteNotFound) + ); + let _ = ctx.delete_user(NAME, PASSWORD).await; } } diff --git a/src/pages/dash/sites/delete.rs b/src/pages/dash/sites/delete.rs new file mode 100644 index 0000000..8d81d91 --- /dev/null +++ b/src/pages/dash/sites/delete.rs @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::cell::RefCell; + +use actix_identity::Identity; +use actix_web::http::header::ContentType; +use serde::{Deserialize, Serialize}; +use tera::Context; +use uuid::Uuid; + +use super::get_auth_middleware; + +use crate::ctx::api::v1::auth::{Login, Password}; +use crate::db::Site; +use crate::pages::dash::TemplateSiteEvent; +use crate::pages::errors::*; +use crate::settings::Settings; +use crate::AppCtx; + +pub use super::*; + +pub const DASH_SITE_DELETE: TemplateFile = + TemplateFile::new("dash_site_delete", "pages/dash/sites/delete.html"); + +const SHOW_DEPLOY_SECRET_KEY: &str = "show_deploy_secret"; + +pub struct Delete { + ctx: RefCell, +} + +impl CtxError for Delete { + fn with_error(&self, e: &ReadableError) -> String { + self.ctx.borrow_mut().insert(ERROR_KEY, e); + self.render() + } +} + +impl Delete { + pub fn new(settings: &Settings, payload: Option) -> Self { + let ctx = RefCell::new(context(settings)); + if let Some(payload) = payload { + ctx.borrow_mut().insert(PAYLOAD_KEY, &payload); + } + + Self { ctx } + } + + pub fn show_deploy_secret(&mut self) { + self.ctx.borrow_mut().insert(SHOW_DEPLOY_SECRET_KEY, &true); + } + + pub fn render(&self) -> String { + TEMPLATES + .render(DASH_SITE_DELETE.name, &self.ctx.borrow()) + .unwrap() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct TemplateSiteWithEvents { + pub site: Site, + pub delete: String, + pub last_update: Option, + pub events: Vec, +} + +impl TemplateSiteWithEvents { + pub fn new( + site: Site, + last_update: Option, + events: Vec, + ) -> Self { + let delete = PAGES.dash.site.get_delete(site.pub_id); + Self { + site, + last_update, + delete, + events, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct DeleteOptions { + show_deploy_secret: Option, +} + +#[actix_web_codegen_const_routes::get( + path = "PAGES.dash.site.delete", + wrap = "get_auth_middleware()" +)] +#[tracing::instrument(name = "Dashboard delete site webpage", skip(ctx, id))] +pub async fn get_delete_site( + ctx: AppCtx, + id: Identity, + path: web::Path, + query: web::Query, +) -> PageResult { + let site_id = path.into_inner(); + let owner = id.identity().unwrap(); + + let site = ctx + .db + .get_site_from_pub_id(site_id, owner) + .await + .map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?; + let last_update = ctx + .db + .get_latest_update_event(&site.hostname) + .await + .map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?; + + let last_update = last_update.map(|e| e.into()); + + let mut db_events = ctx + .db + .list_all_site_events(&site.hostname) + .await + .map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?; + + let mut events = Vec::with_capacity(db_events.len()); + for e in db_events.drain(0..) { + events.push(e.into()); + } + + let payload = TemplateSiteWithEvents::new(site, last_update, events); + let mut page = Delete::new(&ctx.settings, Some(payload)); + if let Some(true) = query.show_deploy_secret { + page.show_deploy_secret(); + } + let add = page.render(); + let html = ContentType::html(); + Ok(HttpResponse::Ok().content_type(html).body(add)) +} + +#[actix_web_codegen_const_routes::post( + path = "PAGES.dash.site.delete", + wrap = "get_auth_middleware()" +)] +#[tracing::instrument(name = "Delete site from webpage", skip(ctx, id))] +pub async fn post_delete_site( + ctx: AppCtx, + id: Identity, + path: web::Path, + payload: web::Form, +) -> PageResult { + let site_id = path.into_inner(); + let owner = id.identity().unwrap(); + + let payload = payload.into_inner(); + let msg = Login { + login: owner, + password: payload.password, + }; + ctx.login(&msg) + .await + .map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?; + + ctx.delete_site(msg.login, site_id) + .await + .map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?; + + Ok(HttpResponse::Found() + .append_header((http::header::LOCATION, PAGES.dash.home)) + .finish()) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(get_delete_site); + cfg.service(post_delete_site); +} diff --git a/src/pages/dash/sites/mod.rs b/src/pages/dash/sites/mod.rs index d26d360..2a7621e 100644 --- a/src/pages/dash/sites/mod.rs +++ b/src/pages/dash/sites/mod.rs @@ -21,6 +21,7 @@ pub use super::home::TemplateSite; pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES}; pub mod add; +pub mod delete; pub mod view; pub fn register_templates(t: &mut tera::Tera) { @@ -30,9 +31,13 @@ pub fn register_templates(t: &mut tera::Tera) { view::DASH_SITE_VIEW .register(t) .expect(view::DASH_SITE_VIEW.name); + delete::DASH_SITE_DELETE + .register(t) + .expect(delete::DASH_SITE_DELETE.name); } pub fn services(cfg: &mut web::ServiceConfig) { add::services(cfg); view::services(cfg); + delete::services(cfg); } diff --git a/src/pages/dash/sites/view.rs b/src/pages/dash/sites/view.rs index 6dbe99a..93f06ae 100644 --- a/src/pages/dash/sites/view.rs +++ b/src/pages/dash/sites/view.rs @@ -59,7 +59,7 @@ impl View { } pub fn show_deploy_secret(&mut self) { - self.ctx.borrow_mut().insert(SHOW_DEPLOY_SECRET_KEY, &true); + self.ctx.borrow_mut().insert(SHOW_DEPLOY_SECRET_KEY, &true); } pub fn render(&self) -> String { @@ -73,6 +73,7 @@ impl View { pub struct TemplateSiteWithEvents { pub site: Site, pub view: String, + pub delete: String, pub last_update: Option, pub events: Vec, } @@ -83,20 +84,21 @@ impl TemplateSiteWithEvents { last_update: Option, events: Vec, ) -> Self { - let view = PAGES.dash.site.get_view(site.pub_id.clone()); + let view = PAGES.dash.site.get_view(site.pub_id); + let delete = PAGES.dash.site.get_delete(site.pub_id); Self { site, last_update, view, + delete, events, } } } - #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] pub struct ViewOptions { - show_deploy_secret: Option + show_deploy_secret: Option, } #[actix_web_codegen_const_routes::get( @@ -108,7 +110,7 @@ pub async fn get_view_site( ctx: AppCtx, id: Identity, path: web::Path, - query: web::Query + query: web::Query, ) -> PageResult { let site_id = path.into_inner(); let owner = id.identity().unwrap(); diff --git a/src/pages/routes.rs b/src/pages/routes.rs index 56352b5..d2a7870 100644 --- a/src/pages/routes.rs +++ b/src/pages/routes.rs @@ -90,6 +90,8 @@ pub struct DashSite { pub add: &'static str, /// view site route pub view: &'static str, + /// delete site route + pub delete: &'static str, } impl DashSite { @@ -97,7 +99,8 @@ impl DashSite { pub const fn new() -> DashSite { let add = "/dash/site/add"; let view = "/dash/site/view/{deployment_pub_id}"; - DashSite { add, view } + let delete = "/dash/site/delete/{deployment_pub_id}"; + DashSite { add, view, delete } } pub fn get_view(&self, deployment_pub_id: Uuid) -> String { @@ -106,6 +109,13 @@ impl DashSite { deployment_pub_id.to_string().as_ref(), ) } + + pub fn get_delete(&self, deployment_pub_id: Uuid) -> String { + self.delete.replace( + "{deployment_pub_id}", + deployment_pub_id.to_string().as_ref(), + ) + } } pub fn get_auth_middleware() -> Authentication { diff --git a/src/static_assets/mod.rs b/src/static_assets/mod.rs index 0c5ae95..1f826c3 100644 --- a/src/static_assets/mod.rs +++ b/src/static_assets/mod.rs @@ -51,7 +51,6 @@ pub mod routes { } } - #[derive(Serialize)] /// Top-level routes data structure for V1 AP1 pub struct Assets { diff --git a/src/tests.rs b/src/tests.rs index c7c308c..92b1b3f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -33,7 +33,7 @@ use crate::page::Page; use crate::settings::Settings; use crate::*; -pub const REPO_URL: &str = "https://github.com/mCaptcha/website/"; +pub const REPO_URL: &str = "http://localhost:8080/mCaptcha/website/"; pub const BRANCH: &str = "gh-pages"; pub async fn get_ctx() -> (Temp, Arc) { @@ -109,6 +109,18 @@ macro_rules! delete_request { ) .await }; + + ($app:expr, $route:expr, $cookies:expr, $serializable:expr, FORM) => { + test::call_service( + &$app, + test::TestRequest::delete() + .uri($route) + .set_form($serializable) + .cookie($cookies) + .to_request(), + ) + .await + }; } #[macro_export] diff --git a/templates/pages/dash/sites/delete.html b/templates/pages/dash/sites/delete.html new file mode 100644 index 0000000..55c6def --- /dev/null +++ b/templates/pages/dash/sites/delete.html @@ -0,0 +1,37 @@ +{% extends 'base' %}{% block title %} Delete {{ payload.site.hostname }}{% endblock title %} {% block nav +%} {% include "auth_nav" %} {% endblock nav %} {% block main %} + +
+
+
+

Confirm access

+

+ Please confirm access to your account to delete deployment at + + {{ payload.site.hostname }} + +

+ + + +
+ +
+
+ +
+
+ +{% endblock main %} diff --git a/templates/pages/dash/sites/view.html b/templates/pages/dash/sites/view.html index e398f4c..bd8aec3 100644 --- a/templates/pages/dash/sites/view.html +++ b/templates/pages/dash/sites/view.html @@ -1,4 +1,4 @@ -{% extends 'base' %}{% block title %} Add Site{% endblock title %} {% block nav +{% extends 'base' %}{% block title %} {{ payload.site.hostname }}{% endblock title %} {% block nav %} {% include "auth_nav" %} {% endblock nav %} {% block main %}
@@ -42,6 +42,7 @@ {% endif %} +