Generate magic link
This commit is contained in:
parent
9617bf08b0
commit
6b6fd54292
@ -8,12 +8,18 @@ use actix_web::web::{Data, ServiceConfig};
|
|||||||
use actix_web::{HttpRequest, HttpResponse};
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
use entities::prelude::Users;
|
use entities::prelude::Users;
|
||||||
use entities::users::Model as User;
|
use entities::users::Model as User;
|
||||||
|
use rand::Rng;
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use sea_orm::prelude::*;
|
use sea_orm::prelude::*;
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use validators::prelude::*;
|
||||||
|
use validators::Validator;
|
||||||
|
use validators_prelude::Host;
|
||||||
|
|
||||||
mod email_check;
|
mod email_check;
|
||||||
|
mod magic_generate;
|
||||||
|
mod magic_sign_in;
|
||||||
mod sign_in;
|
mod sign_in;
|
||||||
mod sign_out;
|
mod sign_out;
|
||||||
mod sign_up;
|
mod sign_up;
|
||||||
@ -32,6 +38,7 @@ pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) {
|
|||||||
.service(sign_in::sign_in)
|
.service(sign_in::sign_in)
|
||||||
.service(sign_up::sign_up)
|
.service(sign_up::sign_up)
|
||||||
.service(sign_out::sign_out)
|
.service(sign_out::sign_out)
|
||||||
|
.service(magic_sign_in::magic_sign_in)
|
||||||
.configure(|c| {
|
.configure(|c| {
|
||||||
social_auth::configure(http_client, c);
|
social_auth::configure(http_client, c);
|
||||||
}),
|
}),
|
||||||
@ -176,3 +183,228 @@ async fn auth_http_response(
|
|||||||
refresh_token,
|
refresh_token,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
pub comment_after_local_part: Option<String>,
|
||||||
|
pub comment_before_domain_part: Option<String>,
|
||||||
|
pub comment_after_domain_part: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_password() -> String {
|
||||||
|
rand::thread_rng()
|
||||||
|
.sample_iter(rand::distributions::Alphanumeric)
|
||||||
|
.take(30)
|
||||||
|
.map(char::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod magic_link {
|
||||||
|
use crate::models::Error;
|
||||||
|
use crate::{http::AuthError, RedisClient};
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use jet_contract::*;
|
||||||
|
use rand::prelude::*;
|
||||||
|
use redis::AsyncCommands;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum AttemptValidity {
|
||||||
|
Allowed,
|
||||||
|
Exhausted,
|
||||||
|
}
|
||||||
|
impl AttemptValidity {
|
||||||
|
pub fn is_exhausted(self) -> bool {
|
||||||
|
matches!(self, Self::Exhausted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn magic_link_key(email: &str) -> String {
|
||||||
|
format!("magic_{email}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_magic_link(
|
||||||
|
email: &str,
|
||||||
|
redis: Data<RedisClient>,
|
||||||
|
) -> Result<(MagicLinkKey, MagicLinkToken, AttemptValidity), Error> {
|
||||||
|
use rand::distributions::Alphanumeric;
|
||||||
|
|
||||||
|
let key = magic_link_key(email);
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let token = format!(
|
||||||
|
"{}-{}-{}",
|
||||||
|
(&mut rng)
|
||||||
|
.sample_iter(Alphanumeric)
|
||||||
|
.take(4)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>(),
|
||||||
|
(&mut rng)
|
||||||
|
.sample_iter(Alphanumeric)
|
||||||
|
.take(4)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>(),
|
||||||
|
(&mut rng)
|
||||||
|
.sample_iter(Alphanumeric)
|
||||||
|
.take(4)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let Ok(mut redis) = redis.get().await else {
|
||||||
|
return Err(AuthError::SerializeMsg.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
if redis
|
||||||
|
.exists(&key)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RedisConnection)?
|
||||||
|
{
|
||||||
|
let attempt: u8 = redis
|
||||||
|
.hget(&key, "current_attempt")
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RedisConnection)?;
|
||||||
|
if attempt + 1 > 2 {
|
||||||
|
let token: String = redis
|
||||||
|
.hget(&key, "token")
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RedisConnection)?;
|
||||||
|
return Ok((
|
||||||
|
MagicLinkKey::new(key),
|
||||||
|
MagicLinkToken::new(token),
|
||||||
|
AttemptValidity::Exhausted,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let _: () = redis
|
||||||
|
.hincr(&key, "current_attempt", 1)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RedisConnection)?;
|
||||||
|
let _: () = redis
|
||||||
|
.hset(&key, "token", &token)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RedisConnection)?;
|
||||||
|
} else {
|
||||||
|
let _: () = redis
|
||||||
|
.hset(&key, "current_attempt", 0)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RedisConnection)?;
|
||||||
|
let _: () = redis
|
||||||
|
.hset_multiple(&key, &[("email", email), ("token", &token)])
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RedisConnection)?;
|
||||||
|
let _: () = redis
|
||||||
|
.expire(&key, 600)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RedisConnection)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
MagicLinkKey::new(key),
|
||||||
|
MagicLinkToken::new(token),
|
||||||
|
AttemptValidity::Allowed,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod create_magic_link_tests {
|
||||||
|
use super::*;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use jet_contract::deadpool_redis;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn full() {
|
||||||
|
let email = "foo@bar.com".to_string();
|
||||||
|
let redis = deadpool_redis::Config::default()
|
||||||
|
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
|
||||||
|
.unwrap();
|
||||||
|
let mut conn = redis.get().await.unwrap();
|
||||||
|
let _: () = conn.del(magic_link_key(&email)).await.unwrap();
|
||||||
|
|
||||||
|
let (key, token, validity) = create_magic_link(&email, Data::new(redis.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let r: Vec<String> = conn.hgetall(&*key).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(validity, r),
|
||||||
|
(
|
||||||
|
AttemptValidity::Allowed,
|
||||||
|
vec![
|
||||||
|
"current_attempt".into(),
|
||||||
|
"0".into(),
|
||||||
|
"email".into(),
|
||||||
|
email.clone(),
|
||||||
|
"token".into(),
|
||||||
|
(&*token).clone()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let (key, token, validity) = create_magic_link(&email, Data::new(redis.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let r: Vec<String> = conn.hgetall(&*key).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(validity, r),
|
||||||
|
(
|
||||||
|
AttemptValidity::Allowed,
|
||||||
|
vec![
|
||||||
|
"current_attempt".into(),
|
||||||
|
"1".into(),
|
||||||
|
"email".into(),
|
||||||
|
email.clone(),
|
||||||
|
"token".into(),
|
||||||
|
token.to_string()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let (key, token, validity) = create_magic_link(&email, Data::new(redis.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let r: Vec<String> = conn.hgetall(&*key).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(validity, r),
|
||||||
|
(
|
||||||
|
AttemptValidity::Allowed,
|
||||||
|
vec![
|
||||||
|
"current_attempt".into(),
|
||||||
|
"2".into(),
|
||||||
|
"email".into(),
|
||||||
|
email.clone(),
|
||||||
|
"token".into(),
|
||||||
|
token.to_string()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let (key, token, validity) = create_magic_link(&email, Data::new(redis.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let r: Vec<String> = conn.hgetall(&*key).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(validity, r),
|
||||||
|
(
|
||||||
|
AttemptValidity::Exhausted,
|
||||||
|
vec![
|
||||||
|
"current_attempt".into(),
|
||||||
|
"2".into(),
|
||||||
|
"email".into(),
|
||||||
|
email.clone(),
|
||||||
|
"token".into(),
|
||||||
|
token.to_string()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,9 +4,6 @@ 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 jet_contract::redis::AsyncCommands;
|
|
||||||
use jet_contract::{MagicLinkKey, MagicLinkToken};
|
|
||||||
use rand::Rng;
|
|
||||||
use sea_orm::prelude::*;
|
use sea_orm::prelude::*;
|
||||||
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter};
|
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -14,6 +11,7 @@ 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::models::*;
|
use crate::models::*;
|
||||||
use crate::{EventBusClient, RedisClient};
|
use crate::{EventBusClient, RedisClient};
|
||||||
|
|
||||||
@ -230,193 +228,3 @@ async fn register(
|
|||||||
is_existing: false,
|
is_existing: false,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
||||||
enum AttemptValidity {
|
|
||||||
Allowed,
|
|
||||||
Exhausted,
|
|
||||||
}
|
|
||||||
impl AttemptValidity {
|
|
||||||
fn is_exhausted(self) -> bool {
|
|
||||||
matches!(self, Self::Exhausted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn magic_link_key(email: &str) -> String {
|
|
||||||
format!("magic_{email}")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_magic_link(
|
|
||||||
email: &str,
|
|
||||||
redis: Data<RedisClient>,
|
|
||||||
) -> Result<(MagicLinkKey, MagicLinkToken, AttemptValidity), Error> {
|
|
||||||
use rand::distributions::Alphanumeric;
|
|
||||||
|
|
||||||
let key = magic_link_key(email);
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
let token = format!(
|
|
||||||
"{}-{}-{}",
|
|
||||||
(&mut rng)
|
|
||||||
.sample_iter(Alphanumeric)
|
|
||||||
.take(4)
|
|
||||||
.map(char::from)
|
|
||||||
.collect::<String>(),
|
|
||||||
(&mut rng)
|
|
||||||
.sample_iter(Alphanumeric)
|
|
||||||
.take(4)
|
|
||||||
.map(char::from)
|
|
||||||
.collect::<String>(),
|
|
||||||
(&mut rng)
|
|
||||||
.sample_iter(Alphanumeric)
|
|
||||||
.take(4)
|
|
||||||
.map(char::from)
|
|
||||||
.collect::<String>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let Ok(mut redis) = redis.get().await else {
|
|
||||||
return Err(AuthError::SerializeMsg.into());
|
|
||||||
};
|
|
||||||
|
|
||||||
if redis
|
|
||||||
.exists(&key)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::RedisConnection)?
|
|
||||||
{
|
|
||||||
let attempt: u8 = redis
|
|
||||||
.hget(&key, "current_attempt")
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::RedisConnection)?;
|
|
||||||
if attempt + 1 > 2 {
|
|
||||||
let token: String = redis
|
|
||||||
.hget(&key, "token")
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::RedisConnection)?;
|
|
||||||
return Ok((
|
|
||||||
MagicLinkKey::new(key),
|
|
||||||
MagicLinkToken::new(token),
|
|
||||||
AttemptValidity::Exhausted,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let _: () = redis
|
|
||||||
.hincr(&key, "current_attempt", 1)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::RedisConnection)?;
|
|
||||||
let _: () = redis
|
|
||||||
.hset(&key, "token", &token)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::RedisConnection)?;
|
|
||||||
} else {
|
|
||||||
let _: () = redis
|
|
||||||
.hset(&key, "current_attempt", 0)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::RedisConnection)?;
|
|
||||||
let _: () = redis
|
|
||||||
.hset_multiple(&key, &[("email", email), ("token", &token)])
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::RedisConnection)?;
|
|
||||||
let _: () = redis
|
|
||||||
.expire(&key, 600)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::RedisConnection)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
MagicLinkKey::new(key),
|
|
||||||
MagicLinkToken::new(token),
|
|
||||||
AttemptValidity::Allowed,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod create_magic_link_tests {
|
|
||||||
use super::*;
|
|
||||||
use actix_web::web::Data;
|
|
||||||
use jet_contract::deadpool_redis;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn full() {
|
|
||||||
let email = "foo@bar.com".to_string();
|
|
||||||
let redis = deadpool_redis::Config::default()
|
|
||||||
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
|
|
||||||
.unwrap();
|
|
||||||
let mut conn = redis.get().await.unwrap();
|
|
||||||
let _: () = conn.del(magic_link_key(&email)).await.unwrap();
|
|
||||||
|
|
||||||
let (key, token, validity) = create_magic_link(&email, Data::new(redis.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let r: Vec<String> = conn.hgetall(&*key).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
(validity, r),
|
|
||||||
(
|
|
||||||
AttemptValidity::Allowed,
|
|
||||||
vec![
|
|
||||||
"current_attempt".into(),
|
|
||||||
"0".into(),
|
|
||||||
"email".into(),
|
|
||||||
email.clone(),
|
|
||||||
"token".into(),
|
|
||||||
(&*token).clone()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let (key, token, validity) = create_magic_link(&email, Data::new(redis.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let r: Vec<String> = conn.hgetall(&*key).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
(validity, r),
|
|
||||||
(
|
|
||||||
AttemptValidity::Allowed,
|
|
||||||
vec![
|
|
||||||
"current_attempt".into(),
|
|
||||||
"1".into(),
|
|
||||||
"email".into(),
|
|
||||||
email.clone(),
|
|
||||||
"token".into(),
|
|
||||||
token.to_string()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let (key, token, validity) = create_magic_link(&email, Data::new(redis.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let r: Vec<String> = conn.hgetall(&*key).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
(validity, r),
|
|
||||||
(
|
|
||||||
AttemptValidity::Allowed,
|
|
||||||
vec![
|
|
||||||
"current_attempt".into(),
|
|
||||||
"2".into(),
|
|
||||||
"email".into(),
|
|
||||||
email.clone(),
|
|
||||||
"token".into(),
|
|
||||||
token.to_string()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let (key, token, validity) = create_magic_link(&email, Data::new(redis.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let r: Vec<String> = conn.hgetall(&*key).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
(validity, r),
|
|
||||||
(
|
|
||||||
AttemptValidity::Exhausted,
|
|
||||||
vec![
|
|
||||||
"current_attempt".into(),
|
|
||||||
"2".into(),
|
|
||||||
"email".into(),
|
|
||||||
email.clone(),
|
|
||||||
"token".into(),
|
|
||||||
token.to_string()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
59
crates/jet-api/src/http/api/authentication/magic_generate.rs
Normal file
59
crates/jet-api/src/http/api/authentication/magic_generate.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use actix_web::{
|
||||||
|
post,
|
||||||
|
web::{Data, Json},
|
||||||
|
HttpRequest, HttpResponse,
|
||||||
|
};
|
||||||
|
use jet_contract::event_bus::{EmailMsg, Topic};
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::{create_user, random_password};
|
||||||
|
use crate::{db_commit, models::*, utils::extract_req_current_site};
|
||||||
|
use crate::{
|
||||||
|
db_t, extractors::RequireInstanceConfigured, models::JsonError, EventBusClient, RedisClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Input {
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/magic-generate")]
|
||||||
|
pub async fn magic_generate(
|
||||||
|
req: HttpRequest,
|
||||||
|
_: RequireInstanceConfigured,
|
||||||
|
payload: Json<Input>,
|
||||||
|
db: Data<DatabaseConnection>,
|
||||||
|
redis: Data<RedisClient>,
|
||||||
|
event_bus: Data<EventBusClient>,
|
||||||
|
) -> Result<HttpResponse, JsonError> {
|
||||||
|
let mut t = db_t!(db)?;
|
||||||
|
let email = payload.into_inner().email;
|
||||||
|
|
||||||
|
let user = match entities::prelude::Users::find()
|
||||||
|
.filter(entities::users::Column::Email.eq(&email))
|
||||||
|
.one(&mut t)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(user)) => user,
|
||||||
|
Ok(None) => create_user(&req, &email, &random_password(), &mut t).await?,
|
||||||
|
Err(e) => return Err(Error::DatabaseError.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
db_commit!(t)?;
|
||||||
|
|
||||||
|
let (key, token, validity) = super::magic_link::create_magic_link(&email, redis).await?;
|
||||||
|
let current_site = extract_req_current_site(&req)?;
|
||||||
|
|
||||||
|
event_bus
|
||||||
|
.publish(Topic::Email, jet_contract::event_bus::Msg::Email(EmailMsg::MagicLink {
|
||||||
|
email,
|
||||||
|
key,
|
||||||
|
token,
|
||||||
|
current_site,
|
||||||
|
}), rumqttc::QoS::AtLeastOnce, true)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(HttpResponse::NotImplemented().finish())
|
||||||
|
}
|
51
crates/jet-api/src/http/api/authentication/magic_sign_in.rs
Normal file
51
crates/jet-api/src/http/api/authentication/magic_sign_in.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use actix_web::{
|
||||||
|
post,
|
||||||
|
web::{Data, Json},
|
||||||
|
HttpRequest, HttpResponse,
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::*;
|
||||||
|
use tracing::error;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{models::{Error, JsonError}, extractors::RequireInstanceConfigured};
|
||||||
|
use crate::{RedisClient, EventBusClient};
|
||||||
|
|
||||||
|
#[post("/magic-sign-in")]
|
||||||
|
async fn magic_sign_in(
|
||||||
|
_: RequireInstanceConfigured,
|
||||||
|
req: HttpRequest,
|
||||||
|
payload: Json<Input>,
|
||||||
|
db: Data<DatabaseConnection>,
|
||||||
|
redis: Data<RedisClient>,
|
||||||
|
event_bus: Data<EventBusClient>,
|
||||||
|
) -> Result<HttpResponse, JsonError> {
|
||||||
|
let mut t = db.begin().await.map_err(|e| {
|
||||||
|
error!("Failed to get database connection: {e}");
|
||||||
|
Error::DatabaseError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match try_magic_sign_in(&mut t).await {
|
||||||
|
Ok(r) => {
|
||||||
|
t.commit().await.map_err(|e| {
|
||||||
|
error!("Failed to commit database changes");
|
||||||
|
JsonError::new("Internal server error")
|
||||||
|
})?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
t.rollback().await.ok();
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_magic_sign_in() -> Result<HttpResponse, JsonError> {}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Input {
|
||||||
|
key: String,
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
*/
|
@ -8,9 +8,7 @@ use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg};
|
|||||||
use jet_contract::UserId;
|
use jet_contract::UserId;
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use rumqttc::QoS;
|
use rumqttc::QoS;
|
||||||
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;
|
||||||
@ -18,6 +16,8 @@ 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 super::EmailAllowComment;
|
||||||
|
|
||||||
#[post("/sign-in")]
|
#[post("/sign-in")]
|
||||||
pub async fn sign_in(
|
pub async fn sign_in(
|
||||||
_: RequireInstanceConfigured,
|
_: RequireInstanceConfigured,
|
||||||
@ -54,24 +54,6 @@ async fn try_sign_in(
|
|||||||
}
|
}
|
||||||
let email = payload.email.trim().to_lowercase();
|
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<String>,
|
|
||||||
pub comment_after_local_part: Option<String>,
|
|
||||||
pub comment_before_domain_part: Option<String>,
|
|
||||||
pub comment_after_domain_part: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let password = payload.password.clone();
|
let password = payload.password.clone();
|
||||||
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}");
|
||||||
|
@ -17,6 +17,9 @@ 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 super::EmailAllowComment;
|
||||||
|
|
||||||
#[post("/sign-up")]
|
#[post("/sign-up")]
|
||||||
pub async fn sign_up(
|
pub async fn sign_up(
|
||||||
@ -54,24 +57,6 @@ async fn try_sign_in(
|
|||||||
}
|
}
|
||||||
let email = payload.email.trim().to_lowercase();
|
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<String>,
|
|
||||||
pub comment_after_local_part: Option<String>,
|
|
||||||
pub comment_before_domain_part: Option<String>,
|
|
||||||
pub comment_after_domain_part: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let password = payload.password.clone();
|
let password = payload.password.clone();
|
||||||
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}");
|
||||||
@ -99,7 +84,8 @@ async fn try_sign_in(
|
|||||||
let user_id = user.id;
|
let user_id = user.id;
|
||||||
let mut user: UserModel = user.into();
|
let mut user: UserModel = user.into();
|
||||||
|
|
||||||
let (ip, user_agent, _current_site) = crate::utils::extract_req_info(&req)?;
|
let ip = extract_req_ip(&req)?;
|
||||||
|
let user_agent = extract_req_uagent(&req)?;
|
||||||
|
|
||||||
user.is_active = Set(true);
|
user.is_active = Set(true);
|
||||||
user.last_active = Set(Some(chrono::Utc::now().fixed_offset()));
|
user.last_active = Set(Some(chrono::Utc::now().fixed_offset()));
|
||||||
|
@ -35,6 +35,7 @@ use sea_orm::{
|
|||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
db_commit, db_rollback, db_t,
|
||||||
extractors::RequireInstanceConfigured,
|
extractors::RequireInstanceConfigured,
|
||||||
http::OAuthError,
|
http::OAuthError,
|
||||||
models::{Error, JsonError},
|
models::{Error, JsonError},
|
||||||
@ -155,7 +156,7 @@ async fn handle_callback(
|
|||||||
use oauth2_signin::web_app::SigninFlowHandleCallbackRet as R;
|
use oauth2_signin::web_app::SigninFlowHandleCallbackRet as R;
|
||||||
let response = match ret {
|
let response = match ret {
|
||||||
R::Ok((access_token_body, user_info)) => {
|
R::Ok((access_token_body, user_info)) => {
|
||||||
let mut tx = db.begin().await.map_err(|_| Error::DatabaseError)?;
|
let mut tx = db_t!(db)?;
|
||||||
match handle_user_info(
|
match handle_user_info(
|
||||||
provider,
|
provider,
|
||||||
req,
|
req,
|
||||||
@ -168,17 +169,11 @@ async fn handle_callback(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
tx.commit().await.map_err(|e| {
|
db_commit!(tx, "Failed to commit social_auth changes to postgres")?;
|
||||||
error!("Failed to commit social_auth changes to postgres: {e}");
|
|
||||||
Error::DatabaseError
|
|
||||||
})?;
|
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tx.rollback().await.map_err(|e| {
|
db_rollback!(tx, "Failed to rollback social_auth changes to postgres");
|
||||||
error!("Failed to rollback social_auth changes to postgres: {e}");
|
|
||||||
Error::DatabaseError
|
|
||||||
})?;
|
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,48 @@ use uuid::Uuid;
|
|||||||
use crate::http::OAuthError;
|
use crate::http::OAuthError;
|
||||||
use crate::{http::AuthError, models::Error};
|
use crate::{http::AuthError, models::Error};
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! db_t {
|
||||||
|
($db: expr) => {{
|
||||||
|
use sea_orm::*;
|
||||||
|
|
||||||
|
$db.begin().await.map_err(|e| {
|
||||||
|
tracing::error!("Failed to start db tracation: {e}");
|
||||||
|
crate::models::Error::DatabaseError
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! db_commit {
|
||||||
|
($db: expr) => {{
|
||||||
|
$db.commit().await.map_err(|e| {
|
||||||
|
tracing::error!("Failed to commit db tracation: {e}");
|
||||||
|
crate::models::Error::DatabaseError
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
($db: expr, $msg: expr) => {{
|
||||||
|
$db.commit().await.map_err(|e| {
|
||||||
|
tracing::error!(std::concat!($msg, ": {}"), e);
|
||||||
|
crate::models::Error::DatabaseError
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! db_rollback {
|
||||||
|
($db: expr) => {{
|
||||||
|
$db.rollback().await.map_err(|e| {
|
||||||
|
tracing::error!("Failed to rollback db tracation: {e}");
|
||||||
|
crate::models::Error::DatabaseError
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
($db: expr, $msg: expr) => {{
|
||||||
|
$db.rollback().await.map_err(|e| {
|
||||||
|
tracing::error!(std::concat!($msg, ": {}"), e);
|
||||||
|
crate::models::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
|
||||||
.peer_addr()
|
.peer_addr()
|
||||||
|
Loading…
Reference in New Issue
Block a user