From 9805a96510649d1c18f46b6cc3c8aea9f275eaa1 Mon Sep 17 00:00:00 2001 From: eraden Date: Sat, 27 Jan 2024 21:23:18 +0100 Subject: [PATCH] Add sign up --- crates/jet-api/src/http/api/authentication.rs | 4 +- .../src/http/api/authentication/sign_in.rs | 18 ++- .../src/http/api/authentication/sign_up.rs | 144 ++++++++++++++++++ crates/jet-api/src/utils/mod.rs | 3 +- 4 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 crates/jet-api/src/http/api/authentication/sign_up.rs diff --git a/crates/jet-api/src/http/api/authentication.rs b/crates/jet-api/src/http/api/authentication.rs index e713ce1..a2f717b 100644 --- a/crates/jet-api/src/http/api/authentication.rs +++ b/crates/jet-api/src/http/api/authentication.rs @@ -15,6 +15,7 @@ use uuid::Uuid; mod email_check; mod sign_in; +mod sign_up; mod social_auth; #[derive(Debug, serde::Serialize)] @@ -27,7 +28,8 @@ pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) { config .service(email_check::email_check) .service(oauth) - .service(sign_in::sign_in); + .service(sign_in::sign_in) + .service(sign_up::sign_up); social_auth::configure(http_client, config); } diff --git a/crates/jet-api/src/http/api/authentication/sign_in.rs b/crates/jet-api/src/http/api/authentication/sign_in.rs index 14ab5de..757c05b 100644 --- a/crates/jet-api/src/http/api/authentication/sign_in.rs +++ b/crates/jet-api/src/http/api/authentication/sign_in.rs @@ -77,25 +77,27 @@ async fn try_sign_in( tracing::error!("Invalid email address: {e}"); return Err(JsonError::new("Please provide a valid email address.")); } + if !config.enable_signup && !has_workspace_invites(&email, &mut *db).await? { + return Err(JsonError::new( + "New account creation is disabled. Please contact your site administrator", + )); + } - let (user, was_created) = match Users::find() + match Users::find() .filter(entities::users::Column::Email.eq(&email)) .one(&mut *db) .await { - Ok(Some(user)) => (user, false), - Ok(None) if !config.enable_signup && !has_workspace_invites(&email, &mut *db).await? => { - return Err(JsonError::new( - "New account creation is disabled. Please contact your site administrator", - )); - } - Ok(None) => (create_user(&req, &email, &password, &mut *db).await?, true), + Ok(None) => {} + Ok(Some(user)) => return Err(JsonError::new("User with this email already exists")), Err(e) => { tracing::error!("Failed to load user for sign-in: {e}"); return Ok(Error::DatabaseError.error_response()); } }; + let user = create_user(&req, &email, &password, db).await?; + let user_id = user.id; let mut user: UserModel = user.into(); diff --git a/crates/jet-api/src/http/api/authentication/sign_up.rs b/crates/jet-api/src/http/api/authentication/sign_up.rs new file mode 100644 index 0000000..d990019 --- /dev/null +++ b/crates/jet-api/src/http/api/authentication/sign_up.rs @@ -0,0 +1,144 @@ +use actix_jwt_session::SessionStorage; +use actix_web::web::{Data, Json}; +use actix_web::ResponseError; +use actix_web::{post, HttpRequest, HttpResponse}; +use entities::prelude::Users; +use entities::users::ActiveModel as UserModel; +use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg}; +use jet_contract::UserId; +use reqwest::StatusCode; +use rumqttc::QoS; +use sea_orm::DatabaseConnection; +use sea_orm::*; +use validators::models::Host; +use validators::prelude::*; + +use crate::config::ApplicationConfig; +use crate::extractors::RequireInstanceConfigured; +use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites}; +use crate::models::{Error, JsonError}; + +#[post("/sign-up")] +pub async fn sign_up( + _: RequireInstanceConfigured, + req: HttpRequest, + payload: Json, + db: Data, + config: Data, + event_bus: Data, + session: Data, +) -> Result { + let mut t = db.begin().await.map_err(|e| { + tracing::error!("Failed to start transaction for sign-in: {e}"); + Error::DatabaseError + })?; + let res = try_sign_in(req, payload, &mut t, config, event_bus, session).await?; + t.commit().await.map_err(|e| { + tracing::error!("Failed to commit transaction for sign-in: {e}"); + Error::DatabaseError + })?; + + Ok(res) +} + +async fn try_sign_in( + req: HttpRequest, + payload: Json, + db: &mut DatabaseTransaction, + config: Data, + event_bus: Data, + session: Data, +) -> Result { + if payload.email.trim().is_empty() || payload.password.trim().is_empty() { + return Err(JsonError::new("Both email and password are required")); + } + let email = payload.email.trim().to_lowercase(); + + #[derive(Validator)] + #[validator(email( + comment(Allow), + ip(Allow), + local(Allow), + at_least_two_labels(Allow), + non_ascii(Allow) + ))] + pub struct EmailAllowComment { + pub local_part: String, + pub need_quoted: bool, + pub domain_part: Host, + pub comment_before_local_part: Option, + pub comment_after_local_part: Option, + pub comment_before_domain_part: Option, + pub comment_after_domain_part: Option, + } + + let password = payload.password.clone(); + if let Err(e) = EmailAllowComment::validate_str(&email) { + tracing::error!("Invalid email address: {e}"); + return Err(JsonError::new("Please provide a valid email address.")); + } + + let (user, was_created) = match Users::find() + .filter(entities::users::Column::Email.eq(&email)) + .one(&mut *db) + .await + { + Ok(Some(user)) => (user, false), + Ok(None) if !config.enable_signup && !has_workspace_invites(&email, &mut *db).await? => { + return Err(JsonError::new( + "New account creation is disabled. Please contact your site administrator", + )); + } + Ok(None) => (create_user(&req, &email, &password, &mut *db).await?, true), + Err(e) => { + tracing::error!("Failed to load user for sign-in: {e}"); + return Ok(Error::DatabaseError.error_response()); + } + }; + + let user_id = user.id; + let mut user: UserModel = user.into(); + + let (ip, user_agent, _current_site) = crate::utils::extract_req_info(&req)?; + + user.is_active = Set(true); + user.last_active = Set(Some(chrono::Utc::now().fixed_offset())); + user.last_login_time = Set(Some(chrono::Utc::now().fixed_offset())); + user.last_login_ip = Set(ip.clone()); + user.last_login_uagent = Set(user_agent.clone()); + user.token_updated_at = Set(Some(chrono::Utc::now().fixed_offset())); + let user = Users::update(user).exec(&mut *db).await.map_err(|e| { + tracing::error!("Failed to update account for {email:?}: {e}"); + Error::DatabaseError + })?; + crate::utils::invites_to_membership(user_id, &email, None, &mut *db).await?; + + if let Err(e) = event_bus + .publish( + Topic::User, + Msg::User(UserMsg::SignIn { + user_id: UserId::new(user_id), + email, + user_agent, + ip, + medium: SignInMedium::Email, + first_time: false, + }), + QoS::AtLeastOnce, + true, + ) + .await + { + tracing::warn!("Failed to publish sign-in msg after sign in: {e}"); + }; + + auth_http_response(user, session, StatusCode::OK) + .await + .map_err(JsonError::new) +} + +#[derive(serde::Deserialize)] +struct SignUpInput { + email: String, + password: String, +} diff --git a/crates/jet-api/src/utils/mod.rs b/crates/jet-api/src/utils/mod.rs index e67a3d1..7916921 100644 --- a/crates/jet-api/src/utils/mod.rs +++ b/crates/jet-api/src/utils/mod.rs @@ -1,4 +1,4 @@ -use actix_web::{HttpRequest, HttpResponse}; +use actix_web::HttpRequest; use chrono::Utc; use entities::project_member_invites::{ Column as ProjectMemberInviteColumn, Model as ProjectMemberInvite, @@ -21,7 +21,6 @@ use tracing::error; use uuid::Uuid; use crate::http::OAuthError; -use crate::models::JsonError; use crate::{http::AuthError, models::Error}; pub fn extract_req_info(req: &HttpRequest) -> Result<(String, String, String), Error> {