From 6b11a582cbb32498803dc68571786a61444f8c9b Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Fri, 9 Feb 2024 21:00:58 +0530 Subject: [PATCH 01/11] fix: remove manifest.json reads from fs fetch --- .../src/manifest/manifest_to_package.rs | 1 + fastn-core/src/package/mod.rs | 3 + fastn-core/src/package/package_doc.rs | 101 +++--------------- 3 files changed, 16 insertions(+), 89 deletions(-) diff --git a/fastn-core/src/manifest/manifest_to_package.rs b/fastn-core/src/manifest/manifest_to_package.rs index f6c718a73d..bd359e8c4a 100644 --- a/fastn-core/src/manifest/manifest_to_package.rs +++ b/fastn-core/src/manifest/manifest_to_package.rs @@ -10,6 +10,7 @@ impl fastn_core::Manifest { package .resolve(&package_root.join(package_name).join("FASTN.ftd"), ds) .await?; + package.files = self.files.keys().map(|f| f.to_string()).collect(); package.auto_import_language( main_package.requested_language.clone(), main_package.selected_language.clone(), diff --git a/fastn-core/src/package/mod.rs b/fastn-core/src/package/mod.rs index a9fa18c904..beccee909f 100644 --- a/fastn-core/src/package/mod.rs +++ b/fastn-core/src/package/mod.rs @@ -8,6 +8,7 @@ pub mod user_group; pub struct Package { pub name: String, /// The `versioned` stores the boolean value storing of the fastn package is versioned or not + pub files: Vec, pub versioned: bool, pub translation_of: Box>, pub translations: Vec, @@ -75,6 +76,7 @@ impl Package { pub fn new(name: &str) -> fastn_core::Package { fastn_core::Package { name: name.to_string(), + files: vec![], versioned: false, translation_of: Box::new(None), translations: vec![], @@ -958,6 +960,7 @@ impl PackageTempIntoPackage for fastn_package::old_fastn::PackageTemp { Package { name: self.name.clone(), + files: vec![], versioned: self.versioned, translation_of: Box::new(translation_of), translations, diff --git a/fastn-core/src/package/package_doc.rs b/fastn-core/src/package/package_doc.rs index 61c8465797..513b1a5b73 100644 --- a/fastn-core/src/package/package_doc.rs +++ b/fastn-core/src/package/package_doc.rs @@ -71,61 +71,27 @@ impl fastn_core::Package { } #[tracing::instrument(skip(self))] - pub(crate) async fn fs_fetch_by_id_using_manifest( + pub(crate) async fn fs_fetch_by_id( &self, id: &str, package_root: Option<&fastn_ds::Path>, ds: &fastn_ds::DocumentStore, - manifest: &fastn_core::Manifest, ) -> fastn_core::Result<(String, Vec)> { let new_id = if fastn_core::file::is_static(id)? { - if manifest.files.contains_key(id.trim_start_matches('/')) { - Some(id.to_string()) - } else { - let new_id = match id.rsplit_once('.') { - Some((remaining, ext)) - if mime_guess::MimeGuess::from_ext(ext) - .first_or_octet_stream() - .to_string() - .starts_with("image/") => - { - if remaining.ends_with("-dark") { - format!( - "{}.{}", - remaining.trim_matches('/').trim_end_matches("-dark"), - ext - ) - } else { - format!("{}-dark.{}", remaining.trim_matches('/'), ext) - } - } - _ => { - tracing::error!(id = id, msg = "id error: can not get the dark"); - return Err(fastn_core::Error::PackageError { - message: format!( - "fs_fetch_by_id:: Corresponding file not found for id: {}. Package: {}", - id, &self.name - ), - }); - } - }; - - if !manifest.files.contains_key(&new_id) { - tracing::error!(id = id, msg = "id error: can not get the dark"); - return Err(fastn_core::Error::PackageError { - message: format!( - "fs_fetch_by_id:: Corresponding file not found for id: {}. Package: {}", - id, &self.name - ), - }); - } - - Some(new_id) + if !self.files.contains(&id.trim_start_matches('/').to_string()) { + return Err(fastn_core::Error::PackageError { + message: format!( + "fs_fetch_by_id:: Corresponding file not found for id: {}. Package: {}", + id, &self.name + ), + }); } + + Some(id.to_string()) } else { file_id_to_names(id) .iter() - .find(|id| manifest.files.contains_key(id.as_str())) + .find(|id| self.files.contains(id)) .map(|id| id.to_string()) }; if let Some(id) = new_id { @@ -150,47 +116,6 @@ impl fastn_core::Package { }) } - #[tracing::instrument(skip(self))] - pub(crate) async fn fs_fetch_by_id( - &self, - id: &str, - package_root: Option<&fastn_ds::Path>, - ds: &fastn_ds::DocumentStore, - manifest: &Option, - ) -> fastn_core::Result<(String, Vec)> { - if let Some(manifest) = manifest { - return self - .fs_fetch_by_id_using_manifest(id, package_root, ds, manifest) - .await; - } - if fastn_core::file::is_static(id)? { - if let Ok(data) = self.fs_fetch_by_file_name(id, package_root, ds).await { - return Ok((id.to_string(), data)); - } - } else { - for name in file_id_to_names(id) { - if let Ok(data) = self - .fs_fetch_by_file_name(name.as_str(), package_root, ds) - .await - { - return Ok((name, data)); - } - } - } - - tracing::error!( - msg = "fs-error: file not found", - document = id, - package = self.name - ); - Err(fastn_core::Error::PackageError { - message: format!( - "fs_fetch_by_id:: Corresponding file not found for id: {}. Package: {}", - id, &self.name - ), - }) - } - #[tracing::instrument(skip_all)] async fn http_fetch_by_file_name(&self, name: &str) -> fastn_core::Result> { let base = self.download_base_url.as_ref().ok_or_else(|| { @@ -390,9 +315,7 @@ impl fastn_core::Package { } } - let manifest = self.get_manifest(ds).await?; - - self.fs_fetch_by_id(id, package_root, ds, &manifest).await + self.fs_fetch_by_id(id, package_root, ds).await } } From 1de47375e34d802246eac5d159b0e60f66ba878e Mon Sep 17 00:00:00 2001 From: Arpita-Jaiswal Date: Tue, 13 Feb 2024 16:27:57 +0530 Subject: [PATCH 02/11] Added fastn_core::http::reload --- fastn-core/src/http.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fastn-core/src/http.rs b/fastn-core/src/http.rs index e603a4db14..1e88079058 100644 --- a/fastn-core/src/http.rs +++ b/fastn-core/src/http.rs @@ -633,6 +633,12 @@ pub(crate) async fn http_get_str(url: &str) -> fastn_core::Result { } } +pub fn reload() -> fastn_core::http::Response { + fastn_core::http::Response::Ok() + .content_type("application/json") + .json(serde_json::json!({"reload": true})) +} + pub fn api_ok(data: impl serde::Serialize) -> fastn_core::Result { #[derive(serde::Serialize)] struct SuccessResponse { From 29242475fb6f0bf75a2b0c5e9e0ea35b49601dc0 Mon Sep 17 00:00:00 2001 From: Arpita-Jaiswal Date: Tue, 13 Feb 2024 16:48:09 +0530 Subject: [PATCH 03/11] Change the json Result --- fastn-core/src/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastn-core/src/http.rs b/fastn-core/src/http.rs index 1e88079058..30fc7829b6 100644 --- a/fastn-core/src/http.rs +++ b/fastn-core/src/http.rs @@ -146,7 +146,7 @@ impl Request { } } - pub fn json(&self) -> fastn_core::Result { + pub fn json(&self) -> serde_json::Result { Ok(serde_json::from_slice(&self.body)?) } From c62e4168578a2fc3eb3dd1d449307ec2469cee69 Mon Sep 17 00:00:00 2001 From: heulitig Date: Tue, 13 Feb 2024 17:38:29 +0530 Subject: [PATCH 04/11] clippy fixes --- fastn-core/src/http.rs | 2 +- ftd/src/ftd2021/html.rs | 2 +- ftd/src/ftd2021/ui.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fastn-core/src/http.rs b/fastn-core/src/http.rs index 30fc7829b6..51c2783090 100644 --- a/fastn-core/src/http.rs +++ b/fastn-core/src/http.rs @@ -147,7 +147,7 @@ impl Request { } pub fn json(&self) -> serde_json::Result { - Ok(serde_json::from_slice(&self.body)?) + serde_json::from_slice(&self.body) } pub fn body_as_json( diff --git a/ftd/src/ftd2021/html.rs b/ftd/src/ftd2021/html.rs index f6eea11d63..3c9067945e 100644 --- a/ftd/src/ftd2021/html.rs +++ b/ftd/src/ftd2021/html.rs @@ -64,7 +64,7 @@ impl Node { let all_children = { let mut children: Vec = self.children.to_vec(); - #[allow(clippy::blocks_in_conditions)] + // #[allow(clippy::blocks_in_conditions)] if let Some(ext_children) = external_children { if *external_open_id == self.attrs.get("data-id").map(|v| { diff --git a/ftd/src/ftd2021/ui.rs b/ftd/src/ftd2021/ui.rs index 3e3df16e1e..e8b3809de6 100644 --- a/ftd/src/ftd2021/ui.rs +++ b/ftd/src/ftd2021/ui.rs @@ -603,7 +603,7 @@ impl Element { _ => return d, }; - #[allow(clippy::blocks_in_conditions)] + // #[allow(clippy::blocks_in_conditions)] if *external_open_id == id.as_ref().map(|v| { if v.contains(':') { From b3fad9b1b22dcfaa735917d7ac3a6a30674b44c8 Mon Sep 17 00:00:00 2001 From: xypnox Date: Tue, 13 Feb 2024 13:25:46 +0530 Subject: [PATCH 05/11] Add neovim syntax highlighting for ftd --- ftd/syntax/neovim-ftd.lua | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 ftd/syntax/neovim-ftd.lua diff --git a/ftd/syntax/neovim-ftd.lua b/ftd/syntax/neovim-ftd.lua new file mode 100644 index 0000000000..7e3cc88eb0 --- /dev/null +++ b/ftd/syntax/neovim-ftd.lua @@ -0,0 +1,38 @@ +-- Function to set up the filetype with the syntax highlight for .ftd files +local function SetupFtdSyntax() + vim.bo.filetype = 'ftd' + + vim.cmd([[ + " Component Declarations, with optional whitespace handling for nested components + syntax match ComponentDeclaration "^\s*--\s\+\(\w\|[-.]\)\+" contained + syntax match ComponentEnd "^\s*--\s*end:\s*\(\w\|[-.]\)\+" contained + " syntax match ComponentDeclaration "^\s*--\s\+\w\+" contained + " syntax match ComponentEnd "^\s*--\s\+end:\s\+\w\+" contained + + " Define a broader match for any line that could contain a key-value pair, if necessary + syntax match ComponentLine "^\s*\w\+[\w\.\-$]*\s*:\s*.*" contains=ComponentKey + + " Match only the key part of a key:value pair + syntax match ComponentKey "^\s*\(\w\|[-.]\)\+\ze:" + + " Comments: Adjusted patterns to ensure correct matching + syntax match ComponentComment "^\s*;;.*" contained + + " Apply contains=ALL to ensure nested components and comments + " are highlighted within parent components + syntax region ComponentStart start=/^\s*--\s\+\w\+/ end=/^\s*--\s\+end:/ contains=ComponentDeclaration,ComponentEnd,ComponentKey,ComponentComment + syntax region ComponentRegion start="pattern" end="pattern" contains=ComponentKey + + " Highlight links + highlight link ComponentDeclaration Tag + highlight link ComponentEnd PreProc + highlight link ComponentKey Identifier + highlight link ComponentComment Comment + ]]) +end + +-- Set up autocommands to apply the custom syntax highlighting for .ftd files +vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { + pattern = "*.ftd", + callback = SetupFtdSyntax, +}) From 33387d723f2086b5a3e15f1ff4e5b80b0eadcb07 Mon Sep 17 00:00:00 2001 From: Amit Upadhyay Date: Tue, 13 Feb 2024 20:05:11 +0530 Subject: [PATCH 06/11] api_ok and api_err returning serde error --- fastn-core/src/auth/email_password.rs | 23 ++++++++++++++++------- fastn-core/src/http.rs | 12 +++++++++--- fastn-core/src/watcher.rs | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/fastn-core/src/auth/email_password.rs b/fastn-core/src/auth/email_password.rs index 3833a5e787..1fd90ef8ee 100644 --- a/fastn-core/src/auth/email_password.rs +++ b/fastn-core/src/auth/email_password.rs @@ -355,17 +355,20 @@ pub(crate) async fn confirm_email( if code.is_none() { tracing::info!("finishing response due to bad ?code"); - return fastn_core::http::api_error("Bad Request", fastn_core::http::StatusCode::OK.into()); + return Ok(fastn_core::http::api_error( + "Bad Request", + fastn_core::http::StatusCode::OK.into(), + )?); } let code = match code.unwrap() { serde_json::Value::String(c) => c, _ => { tracing::info!("failed to Deserialize ?code as string"); - return fastn_core::http::api_error( + return Ok(fastn_core::http::api_error( "Bad Request", fastn_core::http::StatusCode::OK.into(), - ); + )?); } }; @@ -391,7 +394,10 @@ pub(crate) async fn confirm_email( if conf_data.is_none() { tracing::info!("invalid code value. No entry exists for the given code in db"); tracing::info!("provided code: {}", &code); - return fastn_core::http::api_error("Bad Request", fastn_core::http::StatusCode::OK.into()); + return Ok(fastn_core::http::api_error( + "Bad Request", + fastn_core::http::StatusCode::OK.into(), + )?); } let (email_id, session_id, sent_at) = conf_data.unwrap(); @@ -466,16 +472,19 @@ pub(crate) async fn resend_email( let email = req.query().get("email"); if email.is_none() { - return fastn_core::http::api_error("Bad Request", fastn_core::http::StatusCode::OK.into()); + return Ok(fastn_core::http::api_error( + "Bad Request", + fastn_core::http::StatusCode::OK.into(), + )?); } let email = match email.unwrap() { serde_json::Value::String(c) => c.to_owned(), _ => { - return fastn_core::http::api_error( + return Ok(fastn_core::http::api_error( "Bad Request", fastn_core::http::StatusCode::OK.into(), - ); + )?); } }; diff --git a/fastn-core/src/http.rs b/fastn-core/src/http.rs index 51c2783090..1173e3e837 100644 --- a/fastn-core/src/http.rs +++ b/fastn-core/src/http.rs @@ -633,13 +633,19 @@ pub(crate) async fn http_get_str(url: &str) -> fastn_core::Result { } } -pub fn reload() -> fastn_core::http::Response { +pub fn frontend_reload() -> fastn_core::http::Response { fastn_core::http::Response::Ok() .content_type("application/json") .json(serde_json::json!({"reload": true})) } -pub fn api_ok(data: impl serde::Serialize) -> fastn_core::Result { +pub fn frontend_redirect>(url: T) -> fastn_core::http::Response { + fastn_core::http::Response::Ok() + .content_type("application/json") + .json(serde_json::json!({"redirect": url.as_ref()})) +} + +pub fn api_ok(data: impl serde::Serialize) -> serde_json::Result { #[derive(serde::Serialize)] struct SuccessResponse { data: T, @@ -662,7 +668,7 @@ pub fn api_ok(data: impl serde::Serialize) -> fastn_core::Result>( message: T, status_code: Option, -) -> fastn_core::Result { +) -> serde_json::Result { #[derive(serde::Serialize, Debug)] struct ErrorResponse { message: String, diff --git a/fastn-core/src/watcher.rs b/fastn-core/src/watcher.rs index fc20851dd2..db6e4c36ae 100644 --- a/fastn-core/src/watcher.rs +++ b/fastn-core/src/watcher.rs @@ -92,5 +92,5 @@ pub async fn poll() -> fastn_core::Result { WATCHER.1.send(id).await?; } - fastn_core::http::api_ok(got_something) + Ok(fastn_core::http::api_ok(got_something)?) } From 5708d0526da70ef4156edf8f787059446ebbdc76 Mon Sep 17 00:00:00 2001 From: Amit Upadhyay Date: Tue, 13 Feb 2024 21:19:19 +0530 Subject: [PATCH 07/11] api error is not 500 --- fastn-core/src/auth/email_password.rs | 25 +++++-------------------- fastn-core/src/http.rs | 9 +++------ 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/fastn-core/src/auth/email_password.rs b/fastn-core/src/auth/email_password.rs index 1fd90ef8ee..b5802f6a1b 100644 --- a/fastn-core/src/auth/email_password.rs +++ b/fastn-core/src/auth/email_password.rs @@ -355,20 +355,14 @@ pub(crate) async fn confirm_email( if code.is_none() { tracing::info!("finishing response due to bad ?code"); - return Ok(fastn_core::http::api_error( - "Bad Request", - fastn_core::http::StatusCode::OK.into(), - )?); + return Ok(fastn_core::http::api_error("Bad Request")?); } let code = match code.unwrap() { serde_json::Value::String(c) => c, _ => { tracing::info!("failed to Deserialize ?code as string"); - return Ok(fastn_core::http::api_error( - "Bad Request", - fastn_core::http::StatusCode::OK.into(), - )?); + return Ok(fastn_core::http::api_error("Bad Request")?); } }; @@ -394,10 +388,7 @@ pub(crate) async fn confirm_email( if conf_data.is_none() { tracing::info!("invalid code value. No entry exists for the given code in db"); tracing::info!("provided code: {}", &code); - return Ok(fastn_core::http::api_error( - "Bad Request", - fastn_core::http::StatusCode::OK.into(), - )?); + return Ok(fastn_core::http::api_error("Bad Request")?); } let (email_id, session_id, sent_at) = conf_data.unwrap(); @@ -472,19 +463,13 @@ pub(crate) async fn resend_email( let email = req.query().get("email"); if email.is_none() { - return Ok(fastn_core::http::api_error( - "Bad Request", - fastn_core::http::StatusCode::OK.into(), - )?); + return Ok(fastn_core::http::api_error("Bad Request")?); } let email = match email.unwrap() { serde_json::Value::String(c) => c.to_owned(), _ => { - return Ok(fastn_core::http::api_error( - "Bad Request", - fastn_core::http::StatusCode::OK.into(), - )?); + return Ok(fastn_core::http::api_error("Bad Request")?); } }; diff --git a/fastn-core/src/http.rs b/fastn-core/src/http.rs index 1173e3e837..868531fe19 100644 --- a/fastn-core/src/http.rs +++ b/fastn-core/src/http.rs @@ -664,11 +664,8 @@ pub fn api_ok(data: impl serde::Serialize) -> serde_json::Result>( - message: T, - status_code: Option, -) -> serde_json::Result { +/// and `status_code`. Use 200 if `status_code` is None +pub fn api_error>(message: T) -> serde_json::Result { #[derive(serde::Serialize, Debug)] struct ErrorResponse { message: String, @@ -682,7 +679,7 @@ pub fn api_error>( Ok(actix_web::HttpResponse::Ok() .content_type(actix_web::http::header::ContentType::json()) - .status(status_code.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)) + .status(actix_web::http::StatusCode::OK) .body(serde_json::to_string(&resp)?)) } From e84c34eb27c909938bae3ee0583d7c853db7c06a Mon Sep 17 00:00:00 2001 From: Arpita-Jaiswal Date: Tue, 13 Feb 2024 21:51:09 +0530 Subject: [PATCH 08/11] Added frontend_error --- fastn-core/src/http.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/fastn-core/src/http.rs b/fastn-core/src/http.rs index 868531fe19..e4f2c25459 100644 --- a/fastn-core/src/http.rs +++ b/fastn-core/src/http.rs @@ -5,6 +5,13 @@ macro_rules! server_error { }}; } +#[macro_export] +macro_rules! server_error_without_warning { + ($($t:tt)*) => {{ + fastn_core::http::server_error_without_warning(format!($($t)*)) + }}; +} + #[macro_export] macro_rules! unauthorised { ($($t:tt)*) => {{ @@ -21,6 +28,10 @@ macro_rules! not_found { pub fn server_error_(msg: String) -> fastn_core::http::Response { fastn_core::warning!("server error: {}", msg); + server_error_without_warning(msg) +} + +pub fn server_error_without_warning(msg: String) -> fastn_core::http::Response { actix_web::HttpResponse::InternalServerError().body(msg) } @@ -645,6 +656,12 @@ pub fn frontend_redirect>(url: T) -> fastn_core::http::Response { .json(serde_json::json!({"redirect": url.as_ref()})) } +pub fn frontend_error(errors: T) -> fastn_core::http::Response { + fastn_core::http::Response::Ok() + .content_type("application/json") + .json(serde_json::json!({"errors": errors})) +} + pub fn api_ok(data: impl serde::Serialize) -> serde_json::Result { #[derive(serde::Serialize)] struct SuccessResponse { From 7fa94f4620cf9e2583fcd3d0201fab08d6435ec5 Mon Sep 17 00:00:00 2001 From: Amit Upadhyay Date: Wed, 14 Feb 2024 00:29:38 +0530 Subject: [PATCH 09/11] full_path --- fastn-core/src/http.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fastn-core/src/http.rs b/fastn-core/src/http.rs index e4f2c25459..02a46efa18 100644 --- a/fastn-core/src/http.rs +++ b/fastn-core/src/http.rs @@ -108,6 +108,14 @@ pub struct Request { impl Request { //pub fn get_named_params() -> {} + pub fn full_path(&self) -> String { + if self.query_string.is_empty() { + self.path.clone() + } else { + format!("{}?{}", self.path, self.query_string) + } + } + pub fn from_actix(req: actix_web::HttpRequest, body: actix_web::web::Bytes) -> Self { let headers = { let mut headers = reqwest::header::HeaderMap::new(); From 0963c1ec5a0aed7d7fe31246a5c3d83aa72d0f5b Mon Sep 17 00:00:00 2001 From: Amit Upadhyay Date: Wed, 14 Feb 2024 13:11:49 +0530 Subject: [PATCH 10/11] email_password refactored into individual files --- fastn-core/src/auth/config.rs | 1 - fastn-core/src/auth/email_password.rs | 686 ------------------ .../src/auth/email_password/confirm_email.rs | 109 +++ .../src/auth/email_password/create_account.rs | 179 +++++ .../create_and_send_confirmation_email.rs | 136 ++++ fastn-core/src/auth/email_password/login.rs | 115 +++ fastn-core/src/auth/email_password/mod.rs | 65 ++ .../src/auth/email_password/onboarding.rs | 53 ++ .../src/auth/email_password/resend_email.rs | 35 + fastn-core/src/auth/email_password/urls.rs | 21 + fastn-core/src/auth/mod.rs | 1 - fastn-core/src/auth/routes.rs | 2 +- 12 files changed, 714 insertions(+), 689 deletions(-) delete mode 100644 fastn-core/src/auth/config.rs delete mode 100644 fastn-core/src/auth/email_password.rs create mode 100644 fastn-core/src/auth/email_password/confirm_email.rs create mode 100644 fastn-core/src/auth/email_password/create_account.rs create mode 100644 fastn-core/src/auth/email_password/create_and_send_confirmation_email.rs create mode 100644 fastn-core/src/auth/email_password/login.rs create mode 100644 fastn-core/src/auth/email_password/mod.rs create mode 100644 fastn-core/src/auth/email_password/onboarding.rs create mode 100644 fastn-core/src/auth/email_password/resend_email.rs create mode 100644 fastn-core/src/auth/email_password/urls.rs diff --git a/fastn-core/src/auth/config.rs b/fastn-core/src/auth/config.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/fastn-core/src/auth/config.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fastn-core/src/auth/email_password.rs b/fastn-core/src/auth/email_password.rs deleted file mode 100644 index b5802f6a1b..0000000000 --- a/fastn-core/src/auth/email_password.rs +++ /dev/null @@ -1,686 +0,0 @@ -pub(crate) async fn create_user( - req: &fastn_core::http::Request, - req_config: &mut fastn_core::RequestConfig, - config: &fastn_core::Config, - db_pool: &fastn_core::db::PgPool, - next: String, -) -> fastn_core::Result { - use diesel::prelude::*; - use diesel_async::RunQueryDsl; - use validator::ValidateArgs; - - if req.method() != "POST" { - let main = fastn_core::Document { - package_name: config.package.name.clone(), - id: "/-/email-confirmation-request-sent".to_string(), - content: email_confirmation_sent_ftd().to_string(), - parent_path: fastn_ds::Path::new("/"), - }; - - let resp = fastn_core::package::package_doc::read_ftd(req_config, &main, "/", false, false) - .await?; - - return Ok(resp.into()); - } - - #[derive(serde::Deserialize, serde::Serialize, validator::Validate, Debug)] - struct UserPayload { - #[validate(length(min = 4, message = "username must be at least 4 character long"))] - username: String, - #[validate(email(message = "invalid email format"))] - email: String, - #[validate(length(min = 1, message = "name must be at least 1 character long"))] - name: String, - #[validate(custom( - function = "fastn_core::auth::validator::validate_strong_password", - arg = "(&'v_a str, &'v_a str, &'v_a str)" - ))] - password: String, - } - - let user_payload = req.json::(); - - if let Err(e) = user_payload { - return fastn_core::http::user_err( - vec![("payload".into(), vec![format!("Invalid payload. Required the request body to contain json. Original error: {:?}", e)])], - fastn_core::http::StatusCode::OK, - ); - } - - let user_payload = user_payload.unwrap(); - - if let Err(e) = user_payload.validate_args(( - user_payload.username.as_str(), - user_payload.email.as_str(), - user_payload.name.as_str(), - )) { - return fastn_core::http::validation_error_to_user_err(e, fastn_core::http::StatusCode::OK); - } - - let mut conn = db_pool - .get() - .await - .map_err(|e| fastn_core::Error::DatabaseError { - message: format!("Failed to get connection to db. {:?}", e), - })?; - - let username_check: i64 = fastn_core::schema::fastn_user::table - .filter(fastn_core::schema::fastn_user::username.eq(&user_payload.username)) - .select(diesel::dsl::count(fastn_core::schema::fastn_user::id)) - .first(&mut conn) - .await?; - - if username_check > 0 { - return fastn_core::http::user_err( - vec![("username".into(), vec!["username already taken".into()])], - fastn_core::http::StatusCode::OK, - ); - } - - let email_check: i64 = fastn_core::schema::fastn_user_email::table - .filter( - fastn_core::schema::fastn_user_email::email - .eq(fastn_core::utils::citext(&user_payload.email)), - ) - .select(diesel::dsl::count(fastn_core::schema::fastn_user_email::id)) - .first(&mut conn) - .await?; - - if email_check > 0 { - return fastn_core::http::user_err( - vec![("email".into(), vec!["email already taken".into()])], - fastn_core::http::StatusCode::OK, - ); - } - - let salt = - argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng); - - let argon2 = argon2::Argon2::default(); - - let hashed_password = - argon2::PasswordHasher::hash_password(&argon2, user_payload.password.as_bytes(), &salt) - .map_err(|e| fastn_core::Error::generic(format!("error in hashing password: {e}")))? - .to_string(); - - let save_user_email_transaction = conn - .build_transaction() - .run(|c| { - Box::pin(async move { - let user = diesel::insert_into(fastn_core::schema::fastn_user::table) - .values(( - fastn_core::schema::fastn_user::username.eq(user_payload.username), - fastn_core::schema::fastn_user::password.eq(hashed_password), - fastn_core::schema::fastn_user::name.eq(user_payload.name), - )) - .returning(fastn_core::auth::FastnUser::as_returning()) - .get_result(c) - .await?; - - tracing::info!("fastn_user created. user_id: {}", &user.id); - - let email: fastn_core::utils::CiString = - diesel::insert_into(fastn_core::schema::fastn_user_email::table) - .values(( - fastn_core::schema::fastn_user_email::user_id.eq(user.id), - fastn_core::schema::fastn_user_email::email - .eq(fastn_core::utils::citext(user_payload.email.as_str())), - fastn_core::schema::fastn_user_email::verified.eq(false), - fastn_core::schema::fastn_user_email::primary.eq(true), - )) - .returning(fastn_core::schema::fastn_user_email::email) - .get_result(c) - .await?; - - Ok::< - (fastn_core::auth::FastnUser, fastn_core::utils::CiString), - diesel::result::Error, - >((user, email)) - }) - }) - .await; - - if let Err(e) = save_user_email_transaction { - return fastn_core::http::user_err( - vec![ - ("email".into(), vec!["invalid email".into()]), - ("detail".into(), vec![format!("{e}")]), - ], - fastn_core::http::StatusCode::OK, - ); - } - - let (user, email) = save_user_email_transaction.expect("expected transaction to yield Some"); - - tracing::info!("fastn_user email inserted"); - - let conf_link = - create_and_send_confirmation_email(email.0.to_string(), db_pool, req, req_config, next) - .await?; - - let resp_body = serde_json::json!({ - "user": user, - "success": true, - "redirect": redirect_url_from_next(req, "/-/auth/create-user/".to_string()), - }); - - let mut resp = actix_web::HttpResponse::Ok(); - - if config.test_command_running { - resp.insert_header(("X-Fastn-Test", "true")) - .insert_header(("X-Fastn-Test-Email-Confirmation-Link", conf_link)); - } - - Ok(resp.json(resp_body)) -} - -pub(crate) async fn login( - req: &fastn_core::http::Request, - ds: &fastn_ds::DocumentStore, - db_pool: &fastn_core::db::PgPool, - next: String, -) -> fastn_core::Result { - use diesel::prelude::*; - use diesel_async::RunQueryDsl; - - if req.method() != "POST" { - return Ok(fastn_core::not_found!("invalid route")); - } - - #[derive(serde::Deserialize, validator::Validate, Debug)] - struct Payload { - username: String, - password: String, - } - - let payload = req.json::(); - - if let Err(e) = payload { - return fastn_core::http::user_err( - vec![("payload".into(), vec![format!("invalid payload: {:?}", e)])], - fastn_core::http::StatusCode::OK, - ); - } - - let payload = payload.unwrap(); - - let mut errors = Vec::new(); - - if payload.username.is_empty() { - errors.push(("username".into(), vec!["username/email is required".into()])); - } - - if payload.password.is_empty() { - errors.push(("password".into(), vec!["password is required".into()])); - } - - if !errors.is_empty() { - return fastn_core::http::user_err(errors, fastn_core::http::StatusCode::OK); - } - - let mut conn = db_pool - .get() - .await - .map_err(|e| fastn_core::Error::DatabaseError { - message: format!("Failed to get connection to db. {:?}", e), - })?; - - let query = fastn_core::schema::fastn_user::table - .inner_join(fastn_core::schema::fastn_user_email::table) - .filter(fastn_core::schema::fastn_user::username.eq(&payload.username)) - .or_filter( - fastn_core::schema::fastn_user_email::email - .eq(fastn_core::utils::citext(&payload.username)), - ) - .select(fastn_core::auth::FastnUser::as_select()); - - let user: Option = query.first(&mut conn).await.optional()?; - - if user.is_none() { - return fastn_core::http::user_err( - vec![("username".into(), vec!["invalid email/username".into()])], - fastn_core::http::StatusCode::OK, - ); - } - - let user = user.expect("expected user to be Some"); - - // OAuth users don't have password - if user.password.is_empty() { - // TODO: create feature to ask if the user wants to convert their account to an email - // password - // or should we redirect them to the oauth provider they used last time? - // redirecting them will require saving the method they used to login which de don't atm - return fastn_core::http::user_err( - vec![("username".into(), vec!["invalid username".into()])], - fastn_core::http::StatusCode::OK, - ); - } - - let parsed_hash = argon2::PasswordHash::new(&user.password) - .map_err(|e| fastn_core::Error::generic(format!("failed to parse hashed password: {e}")))?; - - let password_match = argon2::PasswordVerifier::verify_password( - &argon2::Argon2::default(), - payload.password.as_bytes(), - &parsed_hash, - ); - - if password_match.is_err() { - return fastn_core::http::user_err( - vec![( - "password".into(), - vec!["incorrect username/password".into()], - )], - fastn_core::http::StatusCode::OK, - ); - } - - // TODO: session should store device that was used to login (chrome desktop on windows) - let session_id: i32 = diesel::insert_into(fastn_core::schema::fastn_session::table) - .values((fastn_core::schema::fastn_session::user_id.eq(&user.id),)) - .returning(fastn_core::schema::fastn_session::id) - .get_result(&mut conn) - .await?; - - tracing::info!("session created. session id: {}", &session_id); - - // client has to 'follow' this request - // https://stackoverflow.com/a/39739894 - fastn_core::auth::set_session_cookie_and_redirect_to_next(req, ds, session_id, next).await -} - -pub(crate) async fn onboarding( - req: &fastn_core::http::Request, - req_config: &mut fastn_core::RequestConfig, - config: &fastn_core::Config, - next: String, -) -> fastn_core::Result { - // The user is logged in after having verfied their email. This is them first time signing - // in so we render `onboarding_ftd`. - // If this is an old user, the cookie FIRST_TIME_SESSION_COOKIE_NAME won't be set for them - // and this will redirect to `next` which is usually the home page. - if req - .cookie(fastn_core::auth::FIRST_TIME_SESSION_COOKIE_NAME) - .is_none() - { - return Ok(fastn_core::http::redirect_with_code( - redirect_url_from_next(req, next), - 307, - )); - } - - let first_signin_doc = fastn_core::Document { - package_name: config.package.name.clone(), - id: "/-/onboarding".to_string(), - content: onboarding_ftd().to_string(), - parent_path: fastn_ds::Path::new("/"), - }; - - let resp = fastn_core::package::package_doc::read_ftd( - req_config, - &first_signin_doc, - "/", - false, - false, - ) - .await?; - - let mut resp: fastn_core::http::Response = resp.into(); - - // clear the cookie so that subsequent requests redirect to `next` - // this gives the onboarding page a single chance to do the process - resp.add_cookie( - &actix_web::cookie::Cookie::build(fastn_core::auth::FIRST_TIME_SESSION_COOKIE_NAME, "") - .domain(fastn_core::auth::utils::domain(req.connection_info.host())) - .path("/") - .expires(actix_web::cookie::time::OffsetDateTime::now_utc()) - .finish(), - ) - .map_err(|e| fastn_core::Error::generic(format!("failed to set cookie: {e}")))?; - - Ok(resp) -} - -pub(crate) async fn confirm_email( - req: &fastn_core::http::Request, - ds: &fastn_ds::DocumentStore, - db_pool: &fastn_core::db::PgPool, - next: String, -) -> fastn_core::Result { - use diesel::prelude::*; - use diesel_async::RunQueryDsl; - - let code = req.query().get("code"); - - if code.is_none() { - tracing::info!("finishing response due to bad ?code"); - return Ok(fastn_core::http::api_error("Bad Request")?); - } - - let code = match code.unwrap() { - serde_json::Value::String(c) => c, - _ => { - tracing::info!("failed to Deserialize ?code as string"); - return Ok(fastn_core::http::api_error("Bad Request")?); - } - }; - - let mut conn = db_pool - .get() - .await - .map_err(|e| fastn_core::Error::DatabaseError { - message: format!("Failed to get connection to db. {:?}", e), - })?; - - let conf_data: Option<(i32, i32, chrono::DateTime)> = - fastn_core::schema::fastn_email_confirmation::table - .select(( - fastn_core::schema::fastn_email_confirmation::email_id, - fastn_core::schema::fastn_email_confirmation::session_id, - fastn_core::schema::fastn_email_confirmation::sent_at, - )) - .filter(fastn_core::schema::fastn_email_confirmation::key.eq(&code)) - .first(&mut conn) - .await - .optional()?; - - if conf_data.is_none() { - tracing::info!("invalid code value. No entry exists for the given code in db"); - tracing::info!("provided code: {}", &code); - return Ok(fastn_core::http::api_error("Bad Request")?); - } - - let (email_id, session_id, sent_at) = conf_data.unwrap(); - - if key_expired(ds, sent_at).await { - // TODO: this redirect route should be configurable - tracing::info!("provided code has expired."); - return Ok(fastn_core::http::redirect_with_code( - format!( - "{}://{}/-/auth/resend-confirmation-email/", - req.connection_info.scheme(), - req.connection_info.host(), - ), - 302, - )); - } - - diesel::update(fastn_core::schema::fastn_user_email::table) - .set(fastn_core::schema::fastn_user_email::verified.eq(true)) - .filter(fastn_core::schema::fastn_user_email::id.eq(email_id)) - .execute(&mut conn) - .await?; - - let affected = diesel::update(fastn_core::schema::fastn_session::table) - .set(fastn_core::schema::fastn_session::active.eq(true)) - .filter(fastn_core::schema::fastn_session::id.eq(session_id)) - .execute(&mut conn) - .await?; - - tracing::info!("session created, rows affected: {}", affected); - - // Onboarding step is opt-in - let onboarding_enabled = ds.env("FASTN_AUTH_ADD_ONBOARDING_STEP").await.is_ok(); - - let next_path = if onboarding_enabled { - format!("/-/auth/onboarding/?next={}", next) - } else { - next.to_string() - }; - - // redirect to onboarding route with a GET request - let mut resp = - fastn_core::auth::set_session_cookie_and_redirect_to_next(req, ds, session_id, next_path) - .await?; - - if onboarding_enabled { - resp.add_cookie( - &actix_web::cookie::Cookie::build( - fastn_core::auth::FIRST_TIME_SESSION_COOKIE_NAME, - "1", - ) - .domain(fastn_core::auth::utils::domain(req.connection_info.host())) - .path("/") - .finish(), - ) - .map_err(|e| fastn_core::Error::generic(format!("failed to set cookie: {e}")))?; - } - - Ok(resp) -} - -pub(crate) async fn resend_email( - req: &fastn_core::http::Request, - req_config: &mut fastn_core::RequestConfig, - db_pool: &fastn_core::db::PgPool, - next: String, -) -> fastn_core::Result { - // TODO: should be able to use username for this too - // TODO: use req.body and make it POST - // verify email with regex or validator crate - // on GET this handler should render auth.resend-email-page - let email = req.query().get("email"); - - if email.is_none() { - return Ok(fastn_core::http::api_error("Bad Request")?); - } - - let email = match email.unwrap() { - serde_json::Value::String(c) => c.to_owned(), - _ => { - return Ok(fastn_core::http::api_error("Bad Request")?); - } - }; - - create_and_send_confirmation_email(email, db_pool, req, req_config, next.clone()).await?; - - // TODO: there's no GET /-/auth/login/ yet - // the client will have to create one for now - // this path should be configuratble too - Ok(fastn_core::http::redirect_with_code( - redirect_url_from_next(req, next), - 302, - )) -} - -async fn create_and_send_confirmation_email( - email: String, - db_pool: &fastn_core::db::PgPool, - req: &fastn_core::http::Request, - req_config: &mut fastn_core::RequestConfig, - next: String, -) -> fastn_core::Result { - use diesel::prelude::*; - use diesel_async::RunQueryDsl; - - let key = generate_key(64); - - let mut conn = db_pool - .get() - .await - .map_err(|e| fastn_core::Error::DatabaseError { - message: format!("Failed to get connection to db. {:?}", e), - })?; - - let (email_id, user_id): (i32, i32) = fastn_core::schema::fastn_user_email::table - .select(( - fastn_core::schema::fastn_user_email::id, - fastn_core::schema::fastn_user_email::user_id, - )) - .filter( - fastn_core::schema::fastn_user_email::email - .eq(fastn_core::utils::citext(email.as_str())), - ) - .first(&mut conn) - .await?; - - // create a non active fastn_sesion entry for auto login - let session_id: i32 = diesel::insert_into(fastn_core::schema::fastn_session::table) - .values(( - fastn_core::schema::fastn_session::user_id.eq(&user_id), - fastn_core::schema::fastn_session::active.eq(false), - )) - .returning(fastn_core::schema::fastn_session::id) - .get_result(&mut conn) - .await?; - - let stored_key: String = - diesel::insert_into(fastn_core::schema::fastn_email_confirmation::table) - .values(( - fastn_core::schema::fastn_email_confirmation::email_id.eq(email_id), - fastn_core::schema::fastn_email_confirmation::session_id.eq(&session_id), - fastn_core::schema::fastn_email_confirmation::sent_at - .eq(chrono::offset::Utc::now()), - fastn_core::schema::fastn_email_confirmation::key.eq(key), - )) - .returning(fastn_core::schema::fastn_email_confirmation::key) - .get_result(&mut conn) - .await?; - - let confirmation_link = confirmation_link(req, stored_key, next); - - let mailer = fastn_core::mail::Mailer::from_env(&req_config.config.ds).await; - - if mailer.is_err() { - return Err(fastn_core::Error::generic( - "Failed to create mailer from env. Creating mailer requires the following environment variables: \ - \tFASTN_SMTP_USERNAME \ - \tFASTN_SMTP_PASSWORD \ - \tFASTN_SMTP_HOST \ - \tFASTN_SMTP_SENDER_EMAIL \ - \tFASTN_SMTP_SENDER_NAME", - )); - } - - let mailer = mailer.unwrap(); - - let name: String = fastn_core::schema::fastn_user::table - .select(fastn_core::schema::fastn_user::name) - .filter(fastn_core::schema::fastn_user::id.eq(user_id)) - .first(&mut conn) - .await?; - - // To use auth. The package has to have auto import with alias `auth` setup - let path = req_config - .config - .package - .eval_auto_import("auth") - .unwrap() - .to_owned(); - - let path = path - .strip_prefix(format!("{}/", req_config.config.package.name).as_str()) - .unwrap(); - - let content = req_config - .config - .ds - .read_to_string(&fastn_ds::Path::new(format!("{}.ftd", path))) - .await?; - - let auth_doc = fastn_core::Document { - package_name: req_config.config.package.name.clone(), - id: path.to_string(), - content, - parent_path: fastn_ds::Path::new("/"), - }; - - let main_ftd_doc = fastn_core::doc::interpret_helper( - auth_doc.id_with_package().as_str(), - auth_doc.content.as_str(), - req_config, - "/", - false, - 0, - ) - .await?; - - let html_email_templ = format!( - "{}/{}#confirmation-mail-html", - req_config.config.package.name, path - ); - - let html: String = main_ftd_doc.get(&html_email_templ).unwrap(); - - tracing::info!("confirmation link: {}", &confirmation_link); - - mailer - .send_raw( - format!("{} <{}>", name, email) - .parse::() - .unwrap(), - "Verify your email", - confirmation_mail_body(html, &confirmation_link), - ) - .await - .map_err(|e| fastn_core::Error::generic(format!("failed to send email: {e}")))?; - - Ok(confirmation_link) -} - -/// check if it has been 3 days since the verification code was sent -/// can be configured using EMAIL_CONFIRMATION_EXPIRE_DAYS -async fn key_expired(ds: &fastn_ds::DocumentStore, sent_at: chrono::DateTime) -> bool { - let expiry_limit_in_days: u64 = ds - .env("EMAIL_CONFIRMATION_EXPIRE_DAYS") - .await - .unwrap_or("3".to_string()) - .parse() - .expect("EMAIL_CONFIRMATION_EXPIRE_DAYS should be a number"); - - sent_at - .checked_add_days(chrono::Days::new(expiry_limit_in_days)) - .unwrap() - <= chrono::offset::Utc::now() -} - -fn confirmation_mail_body(content: String, link: &str) -> String { - // content will have a placeholder for the link - let content = content.replace("{{link}}", link); - - content.to_string() -} - -fn generate_key(length: usize) -> String { - let mut rng = rand::thread_rng(); - rand::distributions::DistString::sample_string( - &rand::distributions::Alphanumeric, - &mut rng, - length, - ) -} - -fn confirmation_link(req: &fastn_core::http::Request, key: String, next: String) -> String { - format!( - "{}://{}/-/auth/confirm-email/?code={key}&next={}", - req.connection_info.scheme(), - req.connection_info.host(), - next - ) -} - -fn redirect_url_from_next(req: &fastn_core::http::Request, next: String) -> String { - format!( - "{}://{}{}", - req.connection_info.scheme(), - req.connection_info.host(), - next, - ) -} - -fn email_confirmation_sent_ftd() -> &'static str { - r#" - -- import: fastn/processors as pr - - -- auth.email-confirmation-request-sent: - "# -} - -fn onboarding_ftd() -> &'static str { - r#" - -- import: fastn/processors as pr - - -- auth.onboarding: - "# -} diff --git a/fastn-core/src/auth/email_password/confirm_email.rs b/fastn-core/src/auth/email_password/confirm_email.rs new file mode 100644 index 0000000000..a6231e52cc --- /dev/null +++ b/fastn-core/src/auth/email_password/confirm_email.rs @@ -0,0 +1,109 @@ +use crate::auth::email_password::key_expired; + +pub(crate) async fn confirm_email( + req: &fastn_core::http::Request, + ds: &fastn_ds::DocumentStore, + db_pool: &fastn_core::db::PgPool, + next: String, +) -> fastn_core::Result { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let code = req.query().get("code"); + + if code.is_none() { + tracing::info!("finishing response due to bad ?code"); + return Ok(fastn_core::http::api_error("Bad Request")?); + } + + let code = match code.unwrap() { + serde_json::Value::String(c) => c, + _ => { + tracing::info!("failed to Deserialize ?code as string"); + return Ok(fastn_core::http::api_error("Bad Request")?); + } + }; + + let mut conn = db_pool + .get() + .await + .map_err(|e| fastn_core::Error::DatabaseError { + message: format!("Failed to get connection to db. {:?}", e), + })?; + + let conf_data: Option<(i32, i32, chrono::DateTime)> = + fastn_core::schema::fastn_email_confirmation::table + .select(( + fastn_core::schema::fastn_email_confirmation::email_id, + fastn_core::schema::fastn_email_confirmation::session_id, + fastn_core::schema::fastn_email_confirmation::sent_at, + )) + .filter(fastn_core::schema::fastn_email_confirmation::key.eq(&code)) + .first(&mut conn) + .await + .optional()?; + + if conf_data.is_none() { + tracing::info!("invalid code value. No entry exists for the given code in db"); + tracing::info!("provided code: {}", &code); + return Ok(fastn_core::http::api_error("Bad Request")?); + } + + let (email_id, session_id, sent_at) = conf_data.unwrap(); + + if key_expired(ds, sent_at).await { + // TODO: this redirect route should be configurable + tracing::info!("provided code has expired."); + return Ok(fastn_core::http::redirect_with_code( + format!( + "{}://{}/-/auth/resend-confirmation-email/", + req.connection_info.scheme(), + req.connection_info.host(), + ), + 302, + )); + } + + diesel::update(fastn_core::schema::fastn_user_email::table) + .set(fastn_core::schema::fastn_user_email::verified.eq(true)) + .filter(fastn_core::schema::fastn_user_email::id.eq(email_id)) + .execute(&mut conn) + .await?; + + let affected = diesel::update(fastn_core::schema::fastn_session::table) + .set(fastn_core::schema::fastn_session::active.eq(true)) + .filter(fastn_core::schema::fastn_session::id.eq(session_id)) + .execute(&mut conn) + .await?; + + tracing::info!("session created, rows affected: {}", affected); + + // Onboarding step is opt-in + let onboarding_enabled = ds.env("FASTN_AUTH_ADD_ONBOARDING_STEP").await.is_ok(); + + let next_path = if onboarding_enabled { + format!("/-/auth/onboarding/?next={}", next) + } else { + next.to_string() + }; + + // redirect to onboarding route with a GET request + let mut resp = + fastn_core::auth::set_session_cookie_and_redirect_to_next(req, ds, session_id, next_path) + .await?; + + if onboarding_enabled { + resp.add_cookie( + &actix_web::cookie::Cookie::build( + fastn_core::auth::FIRST_TIME_SESSION_COOKIE_NAME, + "1", + ) + .domain(fastn_core::auth::utils::domain(req.connection_info.host())) + .path("/") + .finish(), + ) + .map_err(|e| fastn_core::Error::generic(format!("failed to set cookie: {e}")))?; + } + + Ok(resp) +} diff --git a/fastn-core/src/auth/email_password/create_account.rs b/fastn-core/src/auth/email_password/create_account.rs new file mode 100644 index 0000000000..544e55cd67 --- /dev/null +++ b/fastn-core/src/auth/email_password/create_account.rs @@ -0,0 +1,179 @@ +use crate::auth::email_password::{ + create_and_send_confirmation_email, email_confirmation_sent_ftd, redirect_url_from_next, +}; + +pub(crate) async fn create_account( + req: &fastn_core::http::Request, + req_config: &mut fastn_core::RequestConfig, + config: &fastn_core::Config, + db_pool: &fastn_core::db::PgPool, + next: String, +) -> fastn_core::Result { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use validator::ValidateArgs; + + if req.method() != "POST" { + let main = fastn_core::Document { + package_name: config.package.name.clone(), + id: "/-/email-confirmation-request-sent".to_string(), + content: email_confirmation_sent_ftd().to_string(), + parent_path: fastn_ds::Path::new("/"), + }; + + let resp = fastn_core::package::package_doc::read_ftd(req_config, &main, "/", false, false) + .await?; + + return Ok(resp.into()); + } + + #[derive(serde::Deserialize, serde::Serialize, validator::Validate, Debug)] + struct UserPayload { + #[validate(length(min = 4, message = "username must be at least 4 character long"))] + username: String, + #[validate(email(message = "invalid email format"))] + email: String, + #[validate(length(min = 1, message = "name must be at least 1 character long"))] + name: String, + #[validate(custom( + function = "fastn_core::auth::validator::validate_strong_password", + arg = "(&'v_a str, &'v_a str, &'v_a str)" + ))] + password: String, + } + + let user_payload = req.json::(); + + if let Err(e) = user_payload { + return fastn_core::http::user_err( + vec![("payload".into(), vec![format!("Invalid payload. Required the request body to contain json. Original error: {:?}", e)])], + fastn_core::http::StatusCode::OK, + ); + } + + let user_payload = user_payload.unwrap(); + + if let Err(e) = user_payload.validate_args(( + user_payload.username.as_str(), + user_payload.email.as_str(), + user_payload.name.as_str(), + )) { + return fastn_core::http::validation_error_to_user_err(e, fastn_core::http::StatusCode::OK); + } + + let mut conn = db_pool + .get() + .await + .map_err(|e| fastn_core::Error::DatabaseError { + message: format!("Failed to get connection to db. {:?}", e), + })?; + + let username_check: i64 = fastn_core::schema::fastn_user::table + .filter(fastn_core::schema::fastn_user::username.eq(&user_payload.username)) + .select(diesel::dsl::count(fastn_core::schema::fastn_user::id)) + .first(&mut conn) + .await?; + + if username_check > 0 { + return fastn_core::http::user_err( + vec![("username".into(), vec!["username already taken".into()])], + fastn_core::http::StatusCode::OK, + ); + } + + let email_check: i64 = fastn_core::schema::fastn_user_email::table + .filter( + fastn_core::schema::fastn_user_email::email + .eq(fastn_core::utils::citext(&user_payload.email)), + ) + .select(diesel::dsl::count(fastn_core::schema::fastn_user_email::id)) + .first(&mut conn) + .await?; + + if email_check > 0 { + return fastn_core::http::user_err( + vec![("email".into(), vec!["email already taken".into()])], + fastn_core::http::StatusCode::OK, + ); + } + + let salt = + argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng); + + let argon2 = argon2::Argon2::default(); + + let hashed_password = + argon2::PasswordHasher::hash_password(&argon2, user_payload.password.as_bytes(), &salt) + .map_err(|e| fastn_core::Error::generic(format!("error in hashing password: {e}")))? + .to_string(); + + let save_user_email_transaction = conn + .build_transaction() + .run(|c| { + Box::pin(async move { + let user = diesel::insert_into(fastn_core::schema::fastn_user::table) + .values(( + fastn_core::schema::fastn_user::username.eq(user_payload.username), + fastn_core::schema::fastn_user::password.eq(hashed_password), + fastn_core::schema::fastn_user::name.eq(user_payload.name), + )) + .returning(fastn_core::auth::FastnUser::as_returning()) + .get_result(c) + .await?; + + tracing::info!("fastn_user created. user_id: {}", &user.id); + + let email: fastn_core::utils::CiString = + diesel::insert_into(fastn_core::schema::fastn_user_email::table) + .values(( + fastn_core::schema::fastn_user_email::user_id.eq(user.id), + fastn_core::schema::fastn_user_email::email + .eq(fastn_core::utils::citext(user_payload.email.as_str())), + fastn_core::schema::fastn_user_email::verified.eq(false), + fastn_core::schema::fastn_user_email::primary.eq(true), + )) + .returning(fastn_core::schema::fastn_user_email::email) + .get_result(c) + .await?; + + Ok::< + (fastn_core::auth::FastnUser, fastn_core::utils::CiString), + diesel::result::Error, + >((user, email)) + }) + }) + .await; + + if let Err(e) = save_user_email_transaction { + return fastn_core::http::user_err( + vec![ + ("email".into(), vec!["invalid email".into()]), + ("detail".into(), vec![format!("{e}")]), + ], + fastn_core::http::StatusCode::OK, + ); + } + + let (user, email) = save_user_email_transaction.expect("expected transaction to yield Some"); + + tracing::info!("fastn_user email inserted"); + + let conf_link = + create_and_send_confirmation_email(email.0.to_string(), db_pool, req, req_config, next) + .await?; + + let resp_body = serde_json::json!({ + "user": user, + "success": true, + "redirect": redirect_url_from_next(req, "/-/auth/create-user/".to_string()), + }); + + let mut resp = actix_web::HttpResponse::Ok(); + + if config.test_command_running { + resp.insert_header(("X-Fastn-Test", "true")) + .insert_header(("X-Fastn-Test-Email-Confirmation-Link", conf_link)); + } + + Ok(resp.json(resp_body)) +} diff --git a/fastn-core/src/auth/email_password/create_and_send_confirmation_email.rs b/fastn-core/src/auth/email_password/create_and_send_confirmation_email.rs new file mode 100644 index 0000000000..71c94391ef --- /dev/null +++ b/fastn-core/src/auth/email_password/create_and_send_confirmation_email.rs @@ -0,0 +1,136 @@ +use crate::auth::email_password::{confirmation_link, confirmation_mail_body, generate_key}; + +pub(crate) async fn create_and_send_confirmation_email( + email: String, + db_pool: &fastn_core::db::PgPool, + req: &fastn_core::http::Request, + req_config: &mut fastn_core::RequestConfig, + next: String, +) -> fastn_core::Result { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let key = generate_key(64); + + let mut conn = db_pool + .get() + .await + .map_err(|e| fastn_core::Error::DatabaseError { + message: format!("Failed to get connection to db. {:?}", e), + })?; + + let (email_id, user_id): (i32, i32) = fastn_core::schema::fastn_user_email::table + .select(( + fastn_core::schema::fastn_user_email::id, + fastn_core::schema::fastn_user_email::user_id, + )) + .filter( + fastn_core::schema::fastn_user_email::email + .eq(fastn_core::utils::citext(email.as_str())), + ) + .first(&mut conn) + .await?; + + // create a non active fastn_sesion entry for auto login + let session_id: i32 = diesel::insert_into(fastn_core::schema::fastn_session::table) + .values(( + fastn_core::schema::fastn_session::user_id.eq(&user_id), + fastn_core::schema::fastn_session::active.eq(false), + )) + .returning(fastn_core::schema::fastn_session::id) + .get_result(&mut conn) + .await?; + + let stored_key: String = + diesel::insert_into(fastn_core::schema::fastn_email_confirmation::table) + .values(( + fastn_core::schema::fastn_email_confirmation::email_id.eq(email_id), + fastn_core::schema::fastn_email_confirmation::session_id.eq(&session_id), + fastn_core::schema::fastn_email_confirmation::sent_at + .eq(chrono::offset::Utc::now()), + fastn_core::schema::fastn_email_confirmation::key.eq(key), + )) + .returning(fastn_core::schema::fastn_email_confirmation::key) + .get_result(&mut conn) + .await?; + + let confirmation_link = confirmation_link(req, stored_key, next); + + let mailer = fastn_core::mail::Mailer::from_env(&req_config.config.ds).await; + + if mailer.is_err() { + return Err(fastn_core::Error::generic( + "Failed to create mailer from env. Creating mailer requires the following environment variables: \ + \tFASTN_SMTP_USERNAME \ + \tFASTN_SMTP_PASSWORD \ + \tFASTN_SMTP_HOST \ + \tFASTN_SMTP_SENDER_EMAIL \ + \tFASTN_SMTP_SENDER_NAME", + )); + } + + let mailer = mailer.unwrap(); + + let name: String = fastn_core::schema::fastn_user::table + .select(fastn_core::schema::fastn_user::name) + .filter(fastn_core::schema::fastn_user::id.eq(user_id)) + .first(&mut conn) + .await?; + + // To use auth. The package has to have auto import with alias `auth` setup + let path = req_config + .config + .package + .eval_auto_import("auth") + .unwrap() + .to_owned(); + + let path = path + .strip_prefix(format!("{}/", req_config.config.package.name).as_str()) + .unwrap(); + + let content = req_config + .config + .ds + .read_to_string(&fastn_ds::Path::new(format!("{}.ftd", path))) + .await?; + + let auth_doc = fastn_core::Document { + package_name: req_config.config.package.name.clone(), + id: path.to_string(), + content, + parent_path: fastn_ds::Path::new("/"), + }; + + let main_ftd_doc = fastn_core::doc::interpret_helper( + auth_doc.id_with_package().as_str(), + auth_doc.content.as_str(), + req_config, + "/", + false, + 0, + ) + .await?; + + let html_email_templ = format!( + "{}/{}#confirmation-mail-html", + req_config.config.package.name, path + ); + + let html: String = main_ftd_doc.get(&html_email_templ).unwrap(); + + tracing::info!("confirmation link: {}", &confirmation_link); + + mailer + .send_raw( + format!("{} <{}>", name, email) + .parse::() + .unwrap(), + "Verify your email", + confirmation_mail_body(html, &confirmation_link), + ) + .await + .map_err(|e| fastn_core::Error::generic(format!("failed to send email: {e}")))?; + + Ok(confirmation_link) +} diff --git a/fastn-core/src/auth/email_password/login.rs b/fastn-core/src/auth/email_password/login.rs new file mode 100644 index 0000000000..bf411ccbce --- /dev/null +++ b/fastn-core/src/auth/email_password/login.rs @@ -0,0 +1,115 @@ +pub(crate) async fn login( + req: &fastn_core::http::Request, + ds: &fastn_ds::DocumentStore, + db_pool: &fastn_core::db::PgPool, + next: String, +) -> fastn_core::Result { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + if req.method() != "POST" { + return Ok(fastn_core::not_found!("invalid route")); + } + + #[derive(serde::Deserialize, validator::Validate, Debug)] + struct Payload { + username: String, + password: String, + } + + let payload = req.json::(); + + if let Err(e) = payload { + return fastn_core::http::user_err( + vec![("payload".into(), vec![format!("invalid payload: {:?}", e)])], + fastn_core::http::StatusCode::OK, + ); + } + + let payload = payload.unwrap(); + + let mut errors = Vec::new(); + + if payload.username.is_empty() { + errors.push(("username".into(), vec!["username/email is required".into()])); + } + + if payload.password.is_empty() { + errors.push(("password".into(), vec!["password is required".into()])); + } + + if !errors.is_empty() { + return fastn_core::http::user_err(errors, fastn_core::http::StatusCode::OK); + } + + let mut conn = db_pool + .get() + .await + .map_err(|e| fastn_core::Error::DatabaseError { + message: format!("Failed to get connection to db. {:?}", e), + })?; + + let query = fastn_core::schema::fastn_user::table + .inner_join(fastn_core::schema::fastn_user_email::table) + .filter(fastn_core::schema::fastn_user::username.eq(&payload.username)) + .or_filter( + fastn_core::schema::fastn_user_email::email + .eq(fastn_core::utils::citext(&payload.username)), + ) + .select(fastn_core::auth::FastnUser::as_select()); + + let user: Option = query.first(&mut conn).await.optional()?; + + if user.is_none() { + return fastn_core::http::user_err( + vec![("username".into(), vec!["invalid email/username".into()])], + fastn_core::http::StatusCode::OK, + ); + } + + let user = user.expect("expected user to be Some"); + + // OAuth users don't have password + if user.password.is_empty() { + // TODO: create feature to ask if the user wants to convert their account to an email + // password + // or should we redirect them to the oauth provider they used last time? + // redirecting them will require saving the method they used to login which de don't atm + return fastn_core::http::user_err( + vec![("username".into(), vec!["invalid username".into()])], + fastn_core::http::StatusCode::OK, + ); + } + + let parsed_hash = argon2::PasswordHash::new(&user.password) + .map_err(|e| fastn_core::Error::generic(format!("failed to parse hashed password: {e}")))?; + + let password_match = argon2::PasswordVerifier::verify_password( + &argon2::Argon2::default(), + payload.password.as_bytes(), + &parsed_hash, + ); + + if password_match.is_err() { + return fastn_core::http::user_err( + vec![( + "password".into(), + vec!["incorrect username/password".into()], + )], + fastn_core::http::StatusCode::OK, + ); + } + + // TODO: session should store device that was used to login (chrome desktop on windows) + let session_id: i32 = diesel::insert_into(fastn_core::schema::fastn_session::table) + .values((fastn_core::schema::fastn_session::user_id.eq(&user.id),)) + .returning(fastn_core::schema::fastn_session::id) + .get_result(&mut conn) + .await?; + + tracing::info!("session created. session id: {}", &session_id); + + // client has to 'follow' this request + // https://stackoverflow.com/a/39739894 + fastn_core::auth::set_session_cookie_and_redirect_to_next(req, ds, session_id, next).await +} diff --git a/fastn-core/src/auth/email_password/mod.rs b/fastn-core/src/auth/email_password/mod.rs new file mode 100644 index 0000000000..73834b10e7 --- /dev/null +++ b/fastn-core/src/auth/email_password/mod.rs @@ -0,0 +1,65 @@ +mod confirm_email; +mod create_account; +mod create_and_send_confirmation_email; +mod login; +mod onboarding; +mod resend_email; +mod urls; + +pub(crate) use { + confirm_email::confirm_email, + create_account::create_account, + create_and_send_confirmation_email::create_and_send_confirmation_email, + login::login, + onboarding::onboarding, + resend_email::resend_email, + urls::{confirmation_link, redirect_url_from_next}, +}; + +/// check if it has been 3 days since the verification code was sent +/// can be configured using EMAIL_CONFIRMATION_EXPIRE_DAYS +async fn key_expired(ds: &fastn_ds::DocumentStore, sent_at: chrono::DateTime) -> bool { + let expiry_limit_in_days: u64 = ds + .env("EMAIL_CONFIRMATION_EXPIRE_DAYS") + .await + .unwrap_or("3".to_string()) + .parse() + .expect("EMAIL_CONFIRMATION_EXPIRE_DAYS should be a number"); + + sent_at + .checked_add_days(chrono::Days::new(expiry_limit_in_days)) + .unwrap() + <= chrono::offset::Utc::now() +} + +fn confirmation_mail_body(content: String, link: &str) -> String { + // content will have a placeholder for the link + let content = content.replace("{{link}}", link); + + content.to_string() +} + +fn generate_key(length: usize) -> String { + let mut rng = rand::thread_rng(); + rand::distributions::DistString::sample_string( + &rand::distributions::Alphanumeric, + &mut rng, + length, + ) +} + +fn email_confirmation_sent_ftd() -> &'static str { + r#" + -- import: fastn/processors as pr + + -- auth.email-confirmation-request-sent: + "# +} + +fn onboarding_ftd() -> &'static str { + r#" + -- import: fastn/processors as pr + + -- auth.onboarding: + "# +} diff --git a/fastn-core/src/auth/email_password/onboarding.rs b/fastn-core/src/auth/email_password/onboarding.rs new file mode 100644 index 0000000000..4f5dac3fef --- /dev/null +++ b/fastn-core/src/auth/email_password/onboarding.rs @@ -0,0 +1,53 @@ +use crate::auth::email_password::{onboarding_ftd, redirect_url_from_next}; + +pub(crate) async fn onboarding( + req: &fastn_core::http::Request, + req_config: &mut fastn_core::RequestConfig, + config: &fastn_core::Config, + next: String, +) -> fastn_core::Result { + // The user is logged in after having verfied their email. This is them first time signing + // in so we render `onboarding_ftd`. + // If this is an old user, the cookie FIRST_TIME_SESSION_COOKIE_NAME won't be set for them + // and this will redirect to `next` which is usually the home page. + if req + .cookie(fastn_core::auth::FIRST_TIME_SESSION_COOKIE_NAME) + .is_none() + { + return Ok(fastn_core::http::redirect_with_code( + redirect_url_from_next(req, next), + 307, + )); + } + + let first_signin_doc = fastn_core::Document { + package_name: config.package.name.clone(), + id: "/-/onboarding".to_string(), + content: onboarding_ftd().to_string(), + parent_path: fastn_ds::Path::new("/"), + }; + + let resp = fastn_core::package::package_doc::read_ftd( + req_config, + &first_signin_doc, + "/", + false, + false, + ) + .await?; + + let mut resp: fastn_core::http::Response = resp.into(); + + // clear the cookie so that subsequent requests redirect to `next` + // this gives the onboarding page a single chance to do the process + resp.add_cookie( + &actix_web::cookie::Cookie::build(fastn_core::auth::FIRST_TIME_SESSION_COOKIE_NAME, "") + .domain(fastn_core::auth::utils::domain(req.connection_info.host())) + .path("/") + .expires(actix_web::cookie::time::OffsetDateTime::now_utc()) + .finish(), + ) + .map_err(|e| fastn_core::Error::generic(format!("failed to set cookie: {e}")))?; + + Ok(resp) +} diff --git a/fastn-core/src/auth/email_password/resend_email.rs b/fastn-core/src/auth/email_password/resend_email.rs new file mode 100644 index 0000000000..ff1392bb22 --- /dev/null +++ b/fastn-core/src/auth/email_password/resend_email.rs @@ -0,0 +1,35 @@ +use crate::auth::email_password::{create_and_send_confirmation_email, redirect_url_from_next}; + +pub(crate) async fn resend_email( + req: &fastn_core::http::Request, + req_config: &mut fastn_core::RequestConfig, + db_pool: &fastn_core::db::PgPool, + next: String, +) -> fastn_core::Result { + // TODO: should be able to use username for this too + // TODO: use req.body and make it POST + // verify email with regex or validator crate + // on GET this handler should render auth.resend-email-page + let email = req.query().get("email"); + + if email.is_none() { + return Ok(fastn_core::http::api_error("Bad Request")?); + } + + let email = match email.unwrap() { + serde_json::Value::String(c) => c.to_owned(), + _ => { + return Ok(fastn_core::http::api_error("Bad Request")?); + } + }; + + create_and_send_confirmation_email(email, db_pool, req, req_config, next.clone()).await?; + + // TODO: there's no GET /-/auth/login/ yet + // the client will have to create one for now + // this path should be configuratble too + Ok(fastn_core::http::redirect_with_code( + redirect_url_from_next(req, next), + 302, + )) +} diff --git a/fastn-core/src/auth/email_password/urls.rs b/fastn-core/src/auth/email_password/urls.rs new file mode 100644 index 0000000000..7c9bf4a1fa --- /dev/null +++ b/fastn-core/src/auth/email_password/urls.rs @@ -0,0 +1,21 @@ +pub(crate) fn confirmation_link( + req: &fastn_core::http::Request, + key: String, + next: String, +) -> String { + format!( + "{}://{}/-/auth/confirm-email/?code={key}&next={}", + req.connection_info.scheme(), + req.connection_info.host(), + next + ) +} + +pub(crate) fn redirect_url_from_next(req: &fastn_core::http::Request, next: String) -> String { + format!( + "{}://{}{}", + req.connection_info.scheme(), + req.connection_info.host(), + next, + ) +} diff --git a/fastn-core/src/auth/mod.rs b/fastn-core/src/auth/mod.rs index 8ce67c36c1..c8e32c7f79 100644 --- a/fastn-core/src/auth/mod.rs +++ b/fastn-core/src/auth/mod.rs @@ -1,4 +1,3 @@ -pub(crate) mod config; pub(crate) mod github; pub(crate) mod routes; pub(crate) mod utils; diff --git a/fastn-core/src/auth/routes.rs b/fastn-core/src/auth/routes.rs index 55d4ef4dc7..6f728fa504 100644 --- a/fastn-core/src/auth/routes.rs +++ b/fastn-core/src/auth/routes.rs @@ -95,7 +95,7 @@ pub async fn handle_auth( "/-/auth/logout/" => logout(&req, &req_config.config.ds, pool, next).await, "/-/auth/create-user/" => { - fastn_core::auth::email_password::create_user(&req, req_config, config, pool, next) + fastn_core::auth::email_password::create_account(&req, req_config, config, pool, next) .await } "/-/auth/confirm-email/" => { From fda53c74812c74b65c79cd8a9973c2aa6c0ca277 Mon Sep 17 00:00:00 2001 From: Amit Upadhyay Date: Wed, 14 Feb 2024 13:53:44 +0530 Subject: [PATCH 11/11] auth changes --- .../src/auth/email_password/create_account.rs | 49 +++++++++---------- fastn-core/src/auth/email_password/mod.rs | 4 -- .../src/auth/email_password/onboarding.rs | 2 +- fastn-core/src/auth/routes.rs | 2 +- fastn-core/src/http.rs | 3 +- 5 files changed, 26 insertions(+), 34 deletions(-) diff --git a/fastn-core/src/auth/email_password/create_account.rs b/fastn-core/src/auth/email_password/create_account.rs index 544e55cd67..6ffedec9b3 100644 --- a/fastn-core/src/auth/email_password/create_account.rs +++ b/fastn-core/src/auth/email_password/create_account.rs @@ -2,6 +2,21 @@ use crate::auth::email_password::{ create_and_send_confirmation_email, email_confirmation_sent_ftd, redirect_url_from_next, }; +#[derive(serde::Deserialize, serde::Serialize, validator::Validate, Debug)] +struct UserPayload { + #[validate(length(min = 4, message = "username must be at least 4 character long"))] + username: String, + #[validate(email(message = "invalid email format"))] + email: String, + #[validate(length(min = 1, message = "name must be at least 1 character long"))] + name: String, + #[validate(custom( + function = "fastn_core::auth::validator::validate_strong_password", + arg = "(&'v_a str, &'v_a str, &'v_a str)" + ))] + password: String, +} + pub(crate) async fn create_account( req: &fastn_core::http::Request, req_config: &mut fastn_core::RequestConfig, @@ -16,7 +31,7 @@ pub(crate) async fn create_account( if req.method() != "POST" { let main = fastn_core::Document { package_name: config.package.name.clone(), - id: "/-/email-confirmation-request-sent".to_string(), + id: "/-/email-confirmation-request-sent/".to_string(), content: email_confirmation_sent_ftd().to_string(), parent_path: fastn_ds::Path::new("/"), }; @@ -27,38 +42,20 @@ pub(crate) async fn create_account( return Ok(resp.into()); } - #[derive(serde::Deserialize, serde::Serialize, validator::Validate, Debug)] - struct UserPayload { - #[validate(length(min = 4, message = "username must be at least 4 character long"))] - username: String, - #[validate(email(message = "invalid email format"))] - email: String, - #[validate(length(min = 1, message = "name must be at least 1 character long"))] - name: String, - #[validate(custom( - function = "fastn_core::auth::validator::validate_strong_password", - arg = "(&'v_a str, &'v_a str, &'v_a str)" - ))] - password: String, - } - - let user_payload = req.json::(); - - if let Err(e) = user_payload { - return fastn_core::http::user_err( + let user_payload = match req.json::() { + Ok(p) => p, + Err(e) => return fastn_core::http::user_err( vec![("payload".into(), vec![format!("Invalid payload. Required the request body to contain json. Original error: {:?}", e)])], fastn_core::http::StatusCode::OK, - ); - } - - let user_payload = user_payload.unwrap(); + ) + }; if let Err(e) = user_payload.validate_args(( user_payload.username.as_str(), user_payload.email.as_str(), user_payload.name.as_str(), )) { - return fastn_core::http::validation_error_to_user_err(e, fastn_core::http::StatusCode::OK); + return fastn_core::http::validation_error_to_user_err(e); } let mut conn = db_pool @@ -165,7 +162,7 @@ pub(crate) async fn create_account( let resp_body = serde_json::json!({ "user": user, "success": true, - "redirect": redirect_url_from_next(req, "/-/auth/create-user/".to_string()), + "redirect": redirect_url_from_next(req, "/-/auth/create-account/".to_string()), }); let mut resp = actix_web::HttpResponse::Ok(); diff --git a/fastn-core/src/auth/email_password/mod.rs b/fastn-core/src/auth/email_password/mod.rs index 73834b10e7..d7d2364599 100644 --- a/fastn-core/src/auth/email_password/mod.rs +++ b/fastn-core/src/auth/email_password/mod.rs @@ -50,16 +50,12 @@ fn generate_key(length: usize) -> String { fn email_confirmation_sent_ftd() -> &'static str { r#" - -- import: fastn/processors as pr - -- auth.email-confirmation-request-sent: "# } fn onboarding_ftd() -> &'static str { r#" - -- import: fastn/processors as pr - -- auth.onboarding: "# } diff --git a/fastn-core/src/auth/email_password/onboarding.rs b/fastn-core/src/auth/email_password/onboarding.rs index 4f5dac3fef..5a127fd869 100644 --- a/fastn-core/src/auth/email_password/onboarding.rs +++ b/fastn-core/src/auth/email_password/onboarding.rs @@ -22,7 +22,7 @@ pub(crate) async fn onboarding( let first_signin_doc = fastn_core::Document { package_name: config.package.name.clone(), - id: "/-/onboarding".to_string(), + id: "/-/onboarding/".to_string(), content: onboarding_ftd().to_string(), parent_path: fastn_ds::Path::new("/"), }; diff --git a/fastn-core/src/auth/routes.rs b/fastn-core/src/auth/routes.rs index 6f728fa504..cf9e80e15d 100644 --- a/fastn-core/src/auth/routes.rs +++ b/fastn-core/src/auth/routes.rs @@ -94,7 +94,7 @@ pub async fn handle_auth( } "/-/auth/logout/" => logout(&req, &req_config.config.ds, pool, next).await, - "/-/auth/create-user/" => { + "/-/auth/create-account/" => { fastn_core::auth::email_password::create_account(&req, req_config, config, pool, next) .await } diff --git a/fastn-core/src/http.rs b/fastn-core/src/http.rs index 02a46efa18..28abb23780 100644 --- a/fastn-core/src/http.rs +++ b/fastn-core/src/http.rs @@ -856,7 +856,6 @@ fn test_is_bot() { pub fn validation_error_to_user_err( e: validator::ValidationErrors, - status_code: fastn_core::http::StatusCode, ) -> fastn_core::Result { use itertools::Itertools; @@ -891,7 +890,7 @@ pub fn validation_error_to_user_err( }) .collect(); - fastn_core::http::user_err(converted_error, status_code) + fastn_core::http::user_err(converted_error, fastn_core::http::StatusCode::OK) } #[cfg(test)]