magic sign in
This commit is contained in:
parent
6b6fd54292
commit
ba4e6a377f
@ -1,7 +1,8 @@
|
|||||||
use crate::models::Error;
|
use crate::models::{Error, JsonError};
|
||||||
use crate::session::AppClaims;
|
use crate::session::AppClaims;
|
||||||
use actix_jwt_session::{
|
use actix_jwt_session::{
|
||||||
Duration, Hashing, JwtTtl, RefreshTtl, SessionStorage, JWT_HEADER_NAME, REFRESH_HEADER_NAME,
|
Duration, Hashing, JwtTtl, Pair, RefreshTtl, SessionStorage, JWT_HEADER_NAME,
|
||||||
|
REFRESH_HEADER_NAME,
|
||||||
};
|
};
|
||||||
use actix_web::web::scope;
|
use actix_web::web::scope;
|
||||||
use actix_web::web::{Data, ServiceConfig};
|
use actix_web::web::{Data, ServiceConfig};
|
||||||
@ -31,6 +32,23 @@ pub struct AuthResponseBody {
|
|||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AuthResponseBody {
|
||||||
|
pub fn build(pair: Pair<AppClaims>) -> Result<Self, JsonError> {
|
||||||
|
let access_token = pair.jwt.encode().map_err(|e| {
|
||||||
|
tracing::error!("Failed encode JWT: {e}");
|
||||||
|
JsonError::new(AuthError::EncryptPair)
|
||||||
|
})?;
|
||||||
|
let refresh_token = pair.refresh.encode().map_err(|e| {
|
||||||
|
tracing::error!("Failed encode JWT: {e}");
|
||||||
|
JsonError::new(AuthError::EncryptPair)
|
||||||
|
})?;
|
||||||
|
Ok(Self {
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) {
|
pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) {
|
||||||
config.service(
|
config.service(
|
||||||
scope("")
|
scope("")
|
||||||
@ -89,6 +107,8 @@ pub enum AuthError {
|
|||||||
Oauth(OAuthError),
|
Oauth(OAuthError),
|
||||||
#[display(fmt = "Encrypt password failed")]
|
#[display(fmt = "Encrypt password failed")]
|
||||||
EncryptPass,
|
EncryptPass,
|
||||||
|
#[display(fmt = "Encrypt JWT pair failed")]
|
||||||
|
EncryptPair,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
@ -210,9 +230,30 @@ pub fn random_password() -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod password {
|
||||||
|
const HAS_NUM: u8 = 1;
|
||||||
|
const HAS_UPPER: u8 = 2;
|
||||||
|
const HAS_LOWER: u8 = 4;
|
||||||
|
const HAS_SPECIAL: u8 = 8;
|
||||||
|
|
||||||
|
pub fn is_valid(pass: &str) -> bool {
|
||||||
|
pass.len() >= 8
|
||||||
|
|| pass.chars().fold(0, |memo, c| match c {
|
||||||
|
_ if c.is_numeric() => memo | HAS_NUM,
|
||||||
|
_ if c.is_uppercase() => memo | HAS_UPPER,
|
||||||
|
_ if c.is_lowercase() => memo | HAS_UPPER,
|
||||||
|
_ => memo | HAS_SPECIAL,
|
||||||
|
}) & HAS_NUM
|
||||||
|
& HAS_SPECIAL
|
||||||
|
& HAS_UPPER
|
||||||
|
& HAS_LOWER
|
||||||
|
!= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod magic_link {
|
pub mod magic_link {
|
||||||
use crate::models::Error;
|
use crate::models::Error;
|
||||||
use crate::{http::AuthError, RedisClient};
|
use crate::{http::AuthError, redis_c, RedisClient};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use jet_contract::*;
|
use jet_contract::*;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@ -234,12 +275,24 @@ pub mod magic_link {
|
|||||||
format!("magic_{email}")
|
format!("magic_{email}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn clear_magic_link(email: &str, redis: Data<RedisClient>) -> Result<(), Error> {
|
||||||
|
let Ok(mut redis) = redis.get().await else {
|
||||||
|
return Err(AuthError::SerializeMsg.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = magic_link_key(email);
|
||||||
|
let _: () = redis.del(key).await.map_err(|_| Error::RedisConnection)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_magic_link(
|
pub async fn create_magic_link(
|
||||||
email: &str,
|
email: &str,
|
||||||
redis: Data<RedisClient>,
|
redis: Data<RedisClient>,
|
||||||
) -> Result<(MagicLinkKey, MagicLinkToken, AttemptValidity), Error> {
|
) -> Result<(MagicLinkKey, MagicLinkToken, AttemptValidity), Error> {
|
||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
|
|
||||||
|
let mut redis = redis_c!(redis)?;
|
||||||
|
|
||||||
let key = magic_link_key(email);
|
let key = magic_link_key(email);
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let token = format!(
|
let token = format!(
|
||||||
@ -261,10 +314,6 @@ pub mod magic_link {
|
|||||||
.collect::<String>(),
|
.collect::<String>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let Ok(mut redis) = redis.get().await else {
|
|
||||||
return Err(AuthError::SerializeMsg.into());
|
|
||||||
};
|
|
||||||
|
|
||||||
if redis
|
if redis
|
||||||
.exists(&key)
|
.exists(&key)
|
||||||
.await
|
.await
|
||||||
|
@ -4,7 +4,8 @@ use actix_web::web::{Data, Json};
|
|||||||
use actix_web::{post, HttpRequest, HttpResponse};
|
use actix_web::{post, HttpRequest, HttpResponse};
|
||||||
use entities::prelude::{Users, WorkspaceMemberInvites};
|
use entities::prelude::{Users, WorkspaceMemberInvites};
|
||||||
use entities::users::Model as User;
|
use entities::users::Model as User;
|
||||||
use sea_orm::prelude::*;
|
use jet_contract::event_bus::SignInMedium;
|
||||||
|
use sea_orm::{prelude::*, DatabaseTransaction};
|
||||||
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter};
|
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_email::Email;
|
use serde_email::Email;
|
||||||
@ -12,7 +13,8 @@ use serde_email::Email;
|
|||||||
use crate::config::ApplicationConfig;
|
use crate::config::ApplicationConfig;
|
||||||
use crate::extractors::RequireInstanceConfigured;
|
use crate::extractors::RequireInstanceConfigured;
|
||||||
use crate::http::magic_link::create_magic_link;
|
use crate::http::magic_link::create_magic_link;
|
||||||
use crate::models::*;
|
use crate::utils::user_by_email;
|
||||||
|
use crate::{db_commit, db_rollback, db_t, models::*};
|
||||||
use crate::{EventBusClient, RedisClient};
|
use crate::{EventBusClient, RedisClient};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@ -33,16 +35,24 @@ pub async fn email_check(
|
|||||||
if !serde_email::is_valid_email(&payload.email) {
|
if !serde_email::is_valid_email(&payload.email) {
|
||||||
return Ok(HttpResponse::BadRequest().json(JsonError::new("Email is not valid")));
|
return Ok(HttpResponse::BadRequest().json(JsonError::new("Email is not valid")));
|
||||||
}
|
}
|
||||||
|
let mut t = db_t!(db)?;
|
||||||
|
|
||||||
let user = Users::find()
|
let user = user_by_email(payload.email.as_str(), &mut t).await?;
|
||||||
.filter(entities::users::Column::Email.eq(payload.email.as_str()))
|
|
||||||
.one(&**db)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError)?;
|
|
||||||
|
|
||||||
match user {
|
let res = match user {
|
||||||
Some(user) => handle_existing_user(req, payload, user, app_config, event_bus, redis).await,
|
Some(user) => handle_existing_user(req, payload, user, app_config, event_bus, redis).await,
|
||||||
None => register(req, payload, app_config, event_bus, db, redis).await,
|
None => register(req, payload, app_config, event_bus, &mut t, redis).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
db_commit!(t)?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
db_rollback!(t)?;
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +144,7 @@ async fn register(
|
|||||||
payload: Json<EmailCheckPayload>,
|
payload: Json<EmailCheckPayload>,
|
||||||
app_config: Data<ApplicationConfig>,
|
app_config: Data<ApplicationConfig>,
|
||||||
event_bus: Data<EventBusClient>,
|
event_bus: Data<EventBusClient>,
|
||||||
db: Data<DatabaseConnection>,
|
db: &mut DatabaseTransaction,
|
||||||
redis: Data<RedisClient>,
|
redis: Data<RedisClient>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
use sea_orm::Set;
|
use sea_orm::Set;
|
||||||
@ -142,7 +152,7 @@ async fn register(
|
|||||||
if !app_config.enable_signup
|
if !app_config.enable_signup
|
||||||
&& WorkspaceMemberInvites::find()
|
&& WorkspaceMemberInvites::find()
|
||||||
.filter(entities::workspace_member_invites::Column::Email.eq(payload.email.as_str()))
|
.filter(entities::workspace_member_invites::Column::Email.eq(payload.email.as_str()))
|
||||||
.count(&**db)
|
.count(&mut *db)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::DatabaseError)?
|
.map_err(|_| Error::DatabaseError)?
|
||||||
== 0
|
== 0
|
||||||
@ -167,14 +177,17 @@ async fn register(
|
|||||||
user_timezone: Set("UTC".to_string()),
|
user_timezone: Set("UTC".to_string()),
|
||||||
last_login_ip: Set(ip.to_string()),
|
last_login_ip: Set(ip.to_string()),
|
||||||
last_logout_ip: Set(ip.to_string()),
|
last_logout_ip: Set(ip.to_string()),
|
||||||
last_login_medium: Set("MAGIC_LINK".to_string()),
|
last_login_medium: Set(SignInMedium::MagicLink.as_str().to_owned()),
|
||||||
last_login_uagent: Set(user_agent.clone()),
|
last_login_uagent: Set(user_agent.clone()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if !app_config.enable_magic_link_login {
|
if !app_config.enable_magic_link_login {
|
||||||
return Err(AuthError::MagicLinkOff.into());
|
return Err(AuthError::MagicLinkOff.into());
|
||||||
}
|
}
|
||||||
let user = user.save(&**db).await.map_err(|_| Error::DatabaseError)?;
|
let user = user
|
||||||
|
.save(&mut *db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError)?;
|
||||||
{
|
{
|
||||||
use jet_contract::event_bus::*;
|
use jet_contract::event_bus::*;
|
||||||
let user_id = user.id.clone().unwrap();
|
let user_id = user.id.clone().unwrap();
|
||||||
|
@ -4,14 +4,16 @@ use actix_web::{
|
|||||||
HttpRequest, HttpResponse,
|
HttpRequest, HttpResponse,
|
||||||
};
|
};
|
||||||
use jet_contract::event_bus::{EmailMsg, Topic};
|
use jet_contract::event_bus::{EmailMsg, Topic};
|
||||||
use sea_orm::prelude::*;
|
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
|
use sea_orm::{prelude::*, DatabaseTransaction};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use super::{create_user, random_password};
|
use super::{create_user, random_password};
|
||||||
use crate::{db_commit, models::*, utils::extract_req_current_site};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db_t, extractors::RequireInstanceConfigured, models::JsonError, EventBusClient, RedisClient,
|
db_commit, db_rollback, db_t, extractors::RequireInstanceConfigured, models::JsonError,
|
||||||
|
models::*, utils::extract_req_current_site, EventBusClient, RedisClient,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@ -29,31 +31,56 @@ pub async fn magic_generate(
|
|||||||
event_bus: Data<EventBusClient>,
|
event_bus: Data<EventBusClient>,
|
||||||
) -> Result<HttpResponse, JsonError> {
|
) -> Result<HttpResponse, JsonError> {
|
||||||
let mut t = db_t!(db)?;
|
let mut t = db_t!(db)?;
|
||||||
|
match try_create_magic_link(req, payload, &mut t, redis, event_bus).await {
|
||||||
|
Ok(r) => {
|
||||||
|
db_commit!(t)?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
db_rollback!(t)?;
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_create_magic_link(
|
||||||
|
req: HttpRequest,
|
||||||
|
payload: Json<Input>,
|
||||||
|
t: &mut DatabaseTransaction,
|
||||||
|
redis: Data<RedisClient>,
|
||||||
|
event_bus: Data<EventBusClient>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
let email = payload.into_inner().email;
|
let email = payload.into_inner().email;
|
||||||
|
|
||||||
let user = match entities::prelude::Users::find()
|
let _user = match entities::prelude::Users::find()
|
||||||
.filter(entities::users::Column::Email.eq(&email))
|
.filter(entities::users::Column::Email.eq(&email))
|
||||||
.one(&mut t)
|
.one(&mut *t)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(user)) => user,
|
Ok(Some(user)) => user,
|
||||||
Ok(None) => create_user(&req, &email, &random_password(), &mut t).await?,
|
Ok(None) => create_user(&req, &email, &random_password(), &mut *t).await?,
|
||||||
Err(e) => return Err(Error::DatabaseError.into()),
|
Err(e) => {
|
||||||
|
error!("Failed to connect to database while creating magic link: {e}");
|
||||||
|
return Err(Error::DatabaseError.into());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
db_commit!(t)?;
|
let (key, token, _validity) = super::magic_link::create_magic_link(&email, redis).await?;
|
||||||
|
|
||||||
let (key, token, validity) = super::magic_link::create_magic_link(&email, redis).await?;
|
|
||||||
let current_site = extract_req_current_site(&req)?;
|
let current_site = extract_req_current_site(&req)?;
|
||||||
|
|
||||||
event_bus
|
event_bus
|
||||||
.publish(Topic::Email, jet_contract::event_bus::Msg::Email(EmailMsg::MagicLink {
|
.publish(
|
||||||
email,
|
Topic::Email,
|
||||||
key,
|
jet_contract::event_bus::Msg::Email(EmailMsg::MagicLink {
|
||||||
token,
|
email,
|
||||||
current_site,
|
key: jet_contract::MagicLinkKey::new(key.clone()),
|
||||||
}), rumqttc::QoS::AtLeastOnce, true)
|
token,
|
||||||
|
current_site,
|
||||||
|
}),
|
||||||
|
rumqttc::QoS::AtLeastOnce,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(HttpResponse::NotImplemented().finish())
|
Ok(HttpResponse::Ok().json(json!({ "key": key })))
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,33 @@
|
|||||||
|
use actix_jwt_session::{Duration, JwtTtl, RefreshTtl, SessionStorage};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
post,
|
post,
|
||||||
web::{Data, Json},
|
web::{Data, Json},
|
||||||
HttpRequest, HttpResponse,
|
HttpRequest, HttpResponse,
|
||||||
};
|
};
|
||||||
/*
|
|
||||||
|
use jet_contract::{
|
||||||
|
event_bus::{Msg, SignInMedium, Topic, UserMsg},
|
||||||
|
redis::AsyncCommands,
|
||||||
|
};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use rumqttc::QoS;
|
||||||
use sea_orm::prelude::*;
|
use sea_orm::prelude::*;
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
use tracing::error;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{models::{Error, JsonError}, extractors::RequireInstanceConfigured};
|
use crate::{
|
||||||
use crate::{RedisClient, EventBusClient};
|
db_commit, db_rollback, db_t,
|
||||||
|
extractors::RequireInstanceConfigured,
|
||||||
|
models::{Error, JsonError},
|
||||||
|
redis_c,
|
||||||
|
session::AppClaims,
|
||||||
|
utils::{extract_req_ip, extract_req_uagent, user_by_email},
|
||||||
|
};
|
||||||
|
use crate::{EventBusClient, RedisClient};
|
||||||
|
|
||||||
|
use super::{AuthResponseBody, auth_http_response};
|
||||||
|
|
||||||
#[post("/magic-sign-in")]
|
#[post("/magic-sign-in")]
|
||||||
async fn magic_sign_in(
|
async fn magic_sign_in(
|
||||||
@ -20,32 +37,102 @@ async fn magic_sign_in(
|
|||||||
db: Data<DatabaseConnection>,
|
db: Data<DatabaseConnection>,
|
||||||
redis: Data<RedisClient>,
|
redis: Data<RedisClient>,
|
||||||
event_bus: Data<EventBusClient>,
|
event_bus: Data<EventBusClient>,
|
||||||
|
session: Data<SessionStorage>,
|
||||||
) -> Result<HttpResponse, JsonError> {
|
) -> Result<HttpResponse, JsonError> {
|
||||||
let mut t = db.begin().await.map_err(|e| {
|
let mut t = db_t!(db)?;
|
||||||
error!("Failed to get database connection: {e}");
|
|
||||||
Error::DatabaseError
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match try_magic_sign_in(&mut t).await {
|
match try_magic_sign_in(req, payload, &mut t, redis, event_bus, session).await {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
t.commit().await.map_err(|e| {
|
db_commit!(t)?;
|
||||||
error!("Failed to commit database changes");
|
|
||||||
JsonError::new("Internal server error")
|
|
||||||
})?;
|
|
||||||
Ok(r)
|
Ok(r)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
t.rollback().await.ok();
|
db_rollback!(t).ok();
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_magic_sign_in() -> Result<HttpResponse, JsonError> {}
|
async fn try_magic_sign_in(
|
||||||
|
req: HttpRequest,
|
||||||
|
payload: Json<Input>,
|
||||||
|
t: &mut DatabaseTransaction,
|
||||||
|
redis: Data<RedisClient>,
|
||||||
|
event_bus: Data<EventBusClient>,
|
||||||
|
session: Data<SessionStorage>,
|
||||||
|
) -> Result<HttpResponse, JsonError> {
|
||||||
|
let payload = payload.into_inner();
|
||||||
|
let (key, user_token) = (payload.key.trim(), payload.token.trim());
|
||||||
|
if key.is_empty() || user_token.is_empty() {
|
||||||
|
return Err(JsonError::new("User token and key are required"));
|
||||||
|
}
|
||||||
|
let mut redis = redis_c!(redis)?;
|
||||||
|
|
||||||
|
let token: String = redis.hget(key, "token").await.map_err(|e| {
|
||||||
|
tracing::error!("Failed to read token from redis: {e}");
|
||||||
|
Error::RedisConnection
|
||||||
|
})?;
|
||||||
|
let email: String = redis.hget(key, "email").await.map_err(|e| {
|
||||||
|
tracing::error!("Failed to read token from redis: {e}");
|
||||||
|
Error::RedisConnection
|
||||||
|
})?;
|
||||||
|
if user_token != token {
|
||||||
|
return Err(JsonError::new(
|
||||||
|
"The magic code/link has expired please try again",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let user = user_by_email(&email, &mut *t)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::UserRequired)?;
|
||||||
|
let user_agent = extract_req_uagent(&req)?;
|
||||||
|
let ip = extract_req_ip(&req)?;
|
||||||
|
event_bus
|
||||||
|
.publish(
|
||||||
|
Topic::User,
|
||||||
|
Msg::User(UserMsg::SignIn {
|
||||||
|
user_id: jet_contract::UserId::new(user.id),
|
||||||
|
email,
|
||||||
|
user_agent,
|
||||||
|
ip,
|
||||||
|
medium: SignInMedium::Email,
|
||||||
|
first_time: false,
|
||||||
|
}),
|
||||||
|
QoS::AtLeastOnce,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(JsonError::new)?;
|
||||||
|
|
||||||
|
let user_clone = user.clone();
|
||||||
|
|
||||||
|
let mut user: entities::users::ActiveModel = user.into();
|
||||||
|
user.is_active = Set(true);
|
||||||
|
user.is_email_verified = 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(extract_req_ip(&req)?);
|
||||||
|
user.last_login_uagent = Set(extract_req_uagent(&req)?);
|
||||||
|
user.token_updated_at = Set(Some(chrono::Utc::now().fixed_offset()));
|
||||||
|
user.save(&mut *t).await.map_err(|e| {
|
||||||
|
tracing::error!("Failed to update user: {e}");
|
||||||
|
Error::DatabaseError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
crate::utils::invites_to_membership(
|
||||||
|
user_clone.id,
|
||||||
|
&user_clone.email.as_deref().unwrap_or_default(),
|
||||||
|
None,
|
||||||
|
&mut *t,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
auth_http_response(user_clone, session, StatusCode::OK)
|
||||||
|
.await
|
||||||
|
.map_err(JsonError::new)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct Input {
|
struct Input {
|
||||||
key: String,
|
key: String,
|
||||||
token: String,
|
token: String,
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
@ -15,6 +15,7 @@ use crate::config::ApplicationConfig;
|
|||||||
use crate::extractors::RequireInstanceConfigured;
|
use crate::extractors::RequireInstanceConfigured;
|
||||||
use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites};
|
use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites};
|
||||||
use crate::models::{Error, JsonError};
|
use crate::models::{Error, JsonError};
|
||||||
|
use crate::utils::user_by_email;
|
||||||
|
|
||||||
use super::EmailAllowComment;
|
use super::EmailAllowComment;
|
||||||
|
|
||||||
@ -65,17 +66,8 @@ async fn try_sign_in(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
match Users::find()
|
let None = user_by_email(&email, &mut *db).await? else {
|
||||||
.filter(entities::users::Column::Email.eq(&email))
|
return Err(JsonError::new("User with this email already exists"));
|
||||||
.one(&mut *db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
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 = create_user(&req, &email, &password, db).await?;
|
||||||
|
@ -10,14 +10,13 @@ use reqwest::StatusCode;
|
|||||||
use rumqttc::QoS;
|
use rumqttc::QoS;
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
use validators::models::Host;
|
|
||||||
use validators::prelude::*;
|
use validators::prelude::*;
|
||||||
|
|
||||||
use crate::config::ApplicationConfig;
|
use crate::config::ApplicationConfig;
|
||||||
use crate::extractors::RequireInstanceConfigured;
|
use crate::extractors::RequireInstanceConfigured;
|
||||||
use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites};
|
use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites};
|
||||||
use crate::models::{Error, JsonError};
|
use crate::models::{Error, JsonError};
|
||||||
use crate::utils::{extract_req_info, extract_req_ip, extract_req_uagent};
|
use crate::utils::{extract_req_ip, extract_req_uagent};
|
||||||
|
|
||||||
use super::EmailAllowComment;
|
use super::EmailAllowComment;
|
||||||
|
|
||||||
@ -56,8 +55,12 @@ async fn try_sign_in(
|
|||||||
return Err(JsonError::new("Both email and password are required"));
|
return Err(JsonError::new("Both email and password are required"));
|
||||||
}
|
}
|
||||||
let email = payload.email.trim().to_lowercase();
|
let email = payload.email.trim().to_lowercase();
|
||||||
|
|
||||||
let password = payload.password.clone();
|
let password = payload.password.clone();
|
||||||
|
|
||||||
|
if !super::password::is_valid(&password) {
|
||||||
|
return Err(JsonError::new("Password is too weak. Password must have at least 8 characters and contains small letter, big letter, number and special character"));
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(e) = EmailAllowComment::validate_str(&email) {
|
if let Err(e) = EmailAllowComment::validate_str(&email) {
|
||||||
tracing::error!("Invalid email address: {e}");
|
tracing::error!("Invalid email address: {e}");
|
||||||
return Err(JsonError::new("Please provide a valid email address."));
|
return Err(JsonError::new("Please provide a valid email address."));
|
||||||
|
@ -39,6 +39,7 @@ use crate::{
|
|||||||
extractors::RequireInstanceConfigured,
|
extractors::RequireInstanceConfigured,
|
||||||
http::OAuthError,
|
http::OAuthError,
|
||||||
models::{Error, JsonError},
|
models::{Error, JsonError},
|
||||||
|
utils::user_by_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! oauth_envs {
|
macro_rules! oauth_envs {
|
||||||
@ -173,7 +174,7 @@ async fn handle_callback(
|
|||||||
v
|
v
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
db_rollback!(tx, "Failed to rollback social_auth changes to postgres");
|
db_rollback!(tx, "Failed to rollback social_auth changes to postgres").ok();
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,34 +225,14 @@ async fn handle_user_info(
|
|||||||
raw: _,
|
raw: _,
|
||||||
} = user_info;
|
} = user_info;
|
||||||
|
|
||||||
let Some(email) = email else {
|
let email = email.ok_or(Error::ContactSupport)?;
|
||||||
return Ok(HttpResponse::BadRequest().json(JsonError::new(
|
|
||||||
"Something went wrong. Please try again later or contact the support team.",
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
if !email.contains('@') {
|
if !email.contains('@') {
|
||||||
return Ok(HttpResponse::BadRequest().json(JsonError::new(
|
return Err(Error::ContactSupport);
|
||||||
"Something went wrong. Please try again later or contact the support team.",
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = Users::find()
|
let user = user_by_email(&email, &mut *db).await?;
|
||||||
.filter(UserColumn::Email.eq(&email))
|
|
||||||
.one(&mut *db)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let mut user: UserModel = match user {
|
let mut user: UserModel = user.ok_or(Error::ContactSupport)?.into();
|
||||||
Ok(Some(user)) => user.into(),
|
|
||||||
Ok(None) => {
|
|
||||||
return Ok(HttpResponse::BadRequest().json(JsonError::new(
|
|
||||||
"Something went wrong. Please try again later or contact the support team.",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to find user for oauth {provider} sign-in: {e}");
|
|
||||||
return Ok(HttpResponse::InternalServerError().finish());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (ip, user_agent, _current_site) = crate::utils::extract_req_info(&req)?;
|
let (ip, user_agent, _current_site) = crate::utils::extract_req_info(&req)?;
|
||||||
|
|
||||||
|
@ -48,6 +48,10 @@ pub enum Error {
|
|||||||
AddToWorkspace,
|
AddToWorkspace,
|
||||||
#[display(fmt = "Failed to add user to projects")]
|
#[display(fmt = "Failed to add user to projects")]
|
||||||
AddToProject,
|
AddToProject,
|
||||||
|
#[display(fmt = "Something went wrong. Please try again later or contact the support team.")]
|
||||||
|
ContactSupport,
|
||||||
|
#[display(fmt = "User not found")]
|
||||||
|
UserRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AuthError> for Error {
|
impl From<AuthError> for Error {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use actix_web::web::Data;
|
||||||
use actix_web::HttpRequest;
|
use actix_web::HttpRequest;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use entities::project_member_invites::{
|
use entities::project_member_invites::{
|
||||||
@ -64,6 +65,29 @@ macro_rules! db_rollback {
|
|||||||
})
|
})
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! redis_c {
|
||||||
|
($redis: expr) => {
|
||||||
|
$redis.get().await.map_err(|e| {
|
||||||
|
tracing::error!("Failed to obtain redis connection: {e}");
|
||||||
|
Error::RedisConnection
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn user_by_email(
|
||||||
|
email: &str,
|
||||||
|
db: &mut DatabaseTransaction,
|
||||||
|
) -> Result<Option<entities::users::Model>, Error> {
|
||||||
|
entities::prelude::Users::find()
|
||||||
|
.filter(entities::users::Column::Email.eq(email))
|
||||||
|
.one(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to load users: {e}");
|
||||||
|
Error::DatabaseError
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn extract_req_ip(req: &HttpRequest) -> Result<String, Error> {
|
pub fn extract_req_ip(req: &HttpRequest) -> Result<String, Error> {
|
||||||
Ok(req
|
Ok(req
|
||||||
|
Loading…
Reference in New Issue
Block a user