Sign in
This commit is contained in:
parent
0923427551
commit
70cb64a2e0
217
Cargo.lock
generated
217
Cargo.lock
generated
@ -498,7 +498,7 @@ dependencies = [
|
||||
"attohttpc",
|
||||
"dirs",
|
||||
"log",
|
||||
"quick-xml",
|
||||
"quick-xml 0.26.0",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"thiserror",
|
||||
@ -664,6 +664,17 @@ version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||
|
||||
[[package]]
|
||||
name = "byte-unit"
|
||||
version = "5.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ac19bdf0b2665407c39d82dbc937e951e7e2001609f0fb32edd0af45a2d63e"
|
||||
dependencies = [
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"utf8-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck"
|
||||
version = "0.6.11"
|
||||
@ -829,6 +840,12 @@ version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||
|
||||
[[package]]
|
||||
name = "cow-utils"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
@ -960,6 +977,12 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.10.0"
|
||||
@ -1094,6 +1117,18 @@ version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
|
||||
|
||||
[[package]]
|
||||
name = "educe"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8"
|
||||
dependencies = [
|
||||
"enum-ordinalize",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.9.0"
|
||||
@ -1129,6 +1164,26 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-ordinalize"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5"
|
||||
dependencies = [
|
||||
"enum-ordinalize-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-ordinalize-derive"
|
||||
version = "4.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@ -1740,6 +1795,15 @@ dependencies = [
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.0"
|
||||
@ -1796,6 +1860,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"validators",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1949,6 +2014,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.13"
|
||||
@ -1988,6 +2059,15 @@ version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@ -2327,6 +2407,12 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "oncemutex"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.63"
|
||||
@ -2513,6 +2599,27 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "phonenumber"
|
||||
version = "0.3.3+8.13.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "635f3e6288e4f01c049d89332a031bd74f25d64b6fb94703ca966e819488cd06"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"either",
|
||||
"fnv",
|
||||
"itertools 0.11.0",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"quick-xml 0.28.2",
|
||||
"regex",
|
||||
"regex-cache",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"strum 0.24.1",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.3"
|
||||
@ -2691,6 +2798,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
@ -2847,6 +2963,18 @@ dependencies = [
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-cache"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f7b62d69743b8b94f353b6b7c3deb4c5582828328bcb8d5fedf214373808793"
|
||||
dependencies = [
|
||||
"lru-cache",
|
||||
"oncemutex",
|
||||
"regex",
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
@ -3044,7 +3172,7 @@ dependencies = [
|
||||
"md5",
|
||||
"minidom",
|
||||
"percent-encoding",
|
||||
"quick-xml",
|
||||
"quick-xml 0.26.0",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@ -3155,6 +3283,12 @@ dependencies = [
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||
|
||||
[[package]]
|
||||
name = "rxml"
|
||||
version = "0.9.1"
|
||||
@ -3251,7 +3385,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum",
|
||||
"strum 0.25.0",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tracing",
|
||||
@ -3340,6 +3474,9 @@ name = "semver"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
@ -3628,7 +3765,7 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"itertools 0.12.0",
|
||||
"nom",
|
||||
"unicode_categories",
|
||||
]
|
||||
@ -3855,6 +3992,16 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "str-utils"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60bcb3d541a8fd455189b9e022f27d255d103dafd5087a93cff4c0a156a8b597"
|
||||
dependencies = [
|
||||
"cow-utils",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.4"
|
||||
@ -3866,12 +4013,34 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.24.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
@ -4368,6 +4537,12 @@ version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.7.0"
|
||||
@ -4378,6 +4553,40 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validators"
|
||||
version = "0.25.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57e4dd623e1c294e7d7850097c41863cda2703166c5f58c225c5ca969299fb7a"
|
||||
dependencies = [
|
||||
"byte-unit",
|
||||
"data-encoding",
|
||||
"idna",
|
||||
"phonenumber",
|
||||
"regex",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"str-utils",
|
||||
"url",
|
||||
"validators-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validators-derive"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72377736834d42b3e4029d46058f7ee2dbd44340bed0e82aaffd5c395bb0e6b3"
|
||||
dependencies = [
|
||||
"educe",
|
||||
"enum-ordinalize",
|
||||
"phonenumber",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
@ -41,3 +41,4 @@ reqwest = { version = "0.11.23", default-features = false, features = ["rustls",
|
||||
http-api-isahc-client = { version = "0.2.2", features = ["with-sleep-via-tokio"] }
|
||||
dotenv = "0.15.0"
|
||||
chrono = { version = "0.4.32", default-features = false, features = ["clock", "serde"] }
|
||||
validators = { version = "0.25.3", default-features = false, features = ["email", "derive", "all-validators"] }
|
||||
|
3
crates/jet-api/src/extractors/mod.rs
Normal file
3
crates/jet-api/src/extractors/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod require_instance;
|
||||
|
||||
pub use require_instance::*;
|
69
crates/jet-api/src/extractors/require_instance.rs
Normal file
69
crates/jet-api/src/extractors/require_instance.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use actix_web::{web::Data, FromRequest};
|
||||
use derive_more::*;
|
||||
use futures_util::{future::LocalBoxFuture, FutureExt};
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
#[display(fmt = "{{\"error\":\"Instance is not configured\"}}")]
|
||||
pub struct NoInstance;
|
||||
|
||||
impl actix_web::error::ResponseError for NoInstance {
|
||||
fn status_code(&self) -> reqwest::StatusCode {
|
||||
reqwest::StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deref)]
|
||||
pub struct RequireInstance(pub entities::instances::Model);
|
||||
|
||||
impl FromRequest for RequireInstance {
|
||||
type Error = NoInstance;
|
||||
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(
|
||||
req: &actix_web::HttpRequest,
|
||||
_payload: &mut actix_web::dev::Payload,
|
||||
) -> Self::Future {
|
||||
let db = req.app_data::<Data<DatabaseConnection>>().cloned();
|
||||
async move {
|
||||
let Some(db) = db else {
|
||||
return Err(NoInstance);
|
||||
};
|
||||
use sea_orm::EntityTrait;
|
||||
let Ok(Some(instance)) = entities::prelude::Instances::find().one(&**db).await else {
|
||||
return Err(NoInstance);
|
||||
};
|
||||
Ok(Self(instance))
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deref)]
|
||||
pub struct RequireInstanceConfigured(pub entities::instances::Model);
|
||||
|
||||
impl FromRequest for RequireInstanceConfigured {
|
||||
type Error = NoInstance;
|
||||
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(
|
||||
req: &actix_web::HttpRequest,
|
||||
_payload: &mut actix_web::dev::Payload,
|
||||
) -> Self::Future {
|
||||
let db = req.app_data::<Data<DatabaseConnection>>().cloned();
|
||||
async move {
|
||||
let Some(db) = db else {
|
||||
return Err(NoInstance);
|
||||
};
|
||||
use sea_orm::EntityTrait;
|
||||
let Ok(Some(instance)) = entities::prelude::Instances::find().one(&**db).await else {
|
||||
return Err(NoInstance);
|
||||
};
|
||||
if !instance.is_setup_done {
|
||||
return Err(NoInstance);
|
||||
}
|
||||
return Ok(Self(instance));
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
@ -1,13 +1,33 @@
|
||||
use crate::models::Error;
|
||||
use crate::session::AppClaims;
|
||||
use actix_jwt_session::{
|
||||
Duration, Hashing, JwtTtl, RefreshTtl, SessionStorage, JWT_HEADER_NAME, REFRESH_HEADER_NAME,
|
||||
};
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::{delete, get, HttpResponse};
|
||||
use actix_web::{delete, get, HttpRequest, HttpResponse};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::Model as User;
|
||||
use reqwest::StatusCode;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod email_check;
|
||||
mod sign_in;
|
||||
mod social_auth;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct AuthResponseBody {
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) {
|
||||
config.service(email_check::email_check).service(oauth);
|
||||
config
|
||||
.service(email_check::email_check)
|
||||
.service(oauth)
|
||||
.service(sign_in::sign_in);
|
||||
social_auth::configure(http_client, config);
|
||||
}
|
||||
|
||||
@ -53,6 +73,8 @@ pub enum AuthError {
|
||||
#[display(fmt = "{}", _0)]
|
||||
#[from]
|
||||
Oauth(OAuthError),
|
||||
#[display(fmt = "Encrypt password failed")]
|
||||
EncryptPass,
|
||||
}
|
||||
|
||||
#[get("/social-auth")]
|
||||
@ -60,12 +82,100 @@ async fn oauth(_db: Data<DatabaseConnection>) -> HttpResponse {
|
||||
HttpResponse::NotImplemented().finish()
|
||||
}
|
||||
|
||||
#[get("/sign-in")]
|
||||
async fn sign_in(_db: Data<DatabaseConnection>) -> HttpResponse {
|
||||
HttpResponse::NotImplemented().finish()
|
||||
async fn create_user(
|
||||
req: &HttpRequest,
|
||||
email: &str,
|
||||
password: &str,
|
||||
db: &mut DatabaseTransaction,
|
||||
) -> Result<entities::users::Model, Error> {
|
||||
use entities::users::ActiveModel;
|
||||
|
||||
let (ip, user_agent, _current_site) = crate::utils::extract_req_info(&req)?;
|
||||
let password = Hashing::encrypt(password).map_err(|e| {
|
||||
tracing::error!("Failed to encrypt password: {e}");
|
||||
AuthError::EncryptPass
|
||||
})?;
|
||||
Users::insert(ActiveModel {
|
||||
password: Set(password),
|
||||
email: Set(Some(email.to_string())),
|
||||
display_name: Set(email.to_string()),
|
||||
username: Set(Uuid::new_v4().to_string()),
|
||||
first_name: Set("".to_string()),
|
||||
last_name: Set("".to_string()),
|
||||
last_location: Set("".to_string()),
|
||||
created_location: Set("".to_string()),
|
||||
is_password_autoset: Set(false),
|
||||
token: Set(Uuid::new_v4().to_string()),
|
||||
billing_address_country: Set("".to_string()),
|
||||
user_timezone: Set("UTC".to_string()),
|
||||
last_login_ip: Set(ip.to_string()),
|
||||
last_logout_ip: Set(ip.to_string()),
|
||||
last_login_uagent: Set(user_agent.clone()),
|
||||
is_active: Set(true),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to create account for {email:?}: {e}");
|
||||
Error::DatabaseError
|
||||
})
|
||||
}
|
||||
|
||||
async fn has_workspace_invites(email: &str, db: &mut DatabaseTransaction) -> Result<bool, Error> {
|
||||
entities::prelude::WorkspaceMemberInvites::find()
|
||||
.filter(entities::workspace_member_invites::Column::Email.eq(email))
|
||||
.count(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to count workspace member invites for {email:?}: {e}");
|
||||
Error::DatabaseError
|
||||
})
|
||||
.map(|n| n > 0)
|
||||
}
|
||||
|
||||
#[delete("/sign-out")]
|
||||
async fn sign_out(_db: Data<DatabaseConnection>) -> HttpResponse {
|
||||
HttpResponse::NotImplemented().finish()
|
||||
}
|
||||
|
||||
async fn auth_http_response(
|
||||
user: User,
|
||||
session: Data<SessionStorage>,
|
||||
status: StatusCode,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let claims = AppClaims::from_user(user, JwtTtl::new(Duration::days(99999)));
|
||||
let pair = session
|
||||
.store(
|
||||
claims,
|
||||
JwtTtl::new(Duration::days(99999)),
|
||||
RefreshTtl::new(Duration::days(99999)),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to store session: {e}");
|
||||
Error::DatabaseError
|
||||
})?;
|
||||
let access_token = match pair.jwt.encode() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to store session: {e}");
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
};
|
||||
let refresh_token = match pair.refresh.encode() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to store session: {e}");
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(HttpResponse::build(status)
|
||||
.append_header((JWT_HEADER_NAME, access_token.clone()))
|
||||
.append_header((REFRESH_HEADER_NAME, refresh_token.clone()))
|
||||
.json(AuthResponseBody {
|
||||
access_token,
|
||||
refresh_token,
|
||||
}))
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use super::{AuthError, PublishError};
|
||||
use actix_web::http::header::USER_AGENT;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use entities::prelude::{Instances, Users, WorkspaceMemberInvites};
|
||||
use entities::prelude::{Users, WorkspaceMemberInvites};
|
||||
use entities::users::Model as User;
|
||||
use jet_contract::redis::AsyncCommands;
|
||||
use jet_contract::{MagicLinkKey, MagicLinkToken};
|
||||
@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_email::Email;
|
||||
|
||||
use crate::config::ApplicationConfig;
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::models::*;
|
||||
use crate::{EventBusClient, RedisClient};
|
||||
|
||||
@ -23,6 +24,7 @@ struct EmailCheckPayload {
|
||||
|
||||
#[post("/email-check")]
|
||||
pub async fn email_check(
|
||||
_: RequireInstanceConfigured,
|
||||
req: HttpRequest,
|
||||
payload: Json<EmailCheckPayload>,
|
||||
db: Data<DatabaseConnection>,
|
||||
@ -30,13 +32,6 @@ pub async fn email_check(
|
||||
event_bus: Data<EventBusClient>,
|
||||
redis: Data<RedisClient>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let instance = Instances::find()
|
||||
.one(&**db)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError)?;
|
||||
let _instance = instance
|
||||
.filter(|i| !i.is_setup_done)
|
||||
.ok_or(Error::NotConfigured)?;
|
||||
if !serde_email::is_valid_email(&payload.email) {
|
||||
return Ok(HttpResponse::BadRequest().json(JsonError::new("Email is not valid")));
|
||||
}
|
||||
@ -157,15 +152,7 @@ async fn register(
|
||||
return Err(Error::Auth(AuthError::RegisterOff));
|
||||
}
|
||||
let payload = payload.into_inner();
|
||||
let ip = req.peer_addr().ok_or(AuthError::NoPeerAddr)?.ip();
|
||||
let user_agent = req
|
||||
.headers()
|
||||
.get(USER_AGENT)
|
||||
.ok_or(AuthError::NoUserAgent)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidUserAgent)?
|
||||
.to_string();
|
||||
let current_site = req.uri().host().ok_or(Error::NoHost)?.to_owned();
|
||||
let (ip, user_agent, current_site) = crate::utils::extract_req_info(&req)?;
|
||||
|
||||
let user = entities::users::ActiveModel {
|
||||
password: Set(Uuid::new_v4().to_string()),
|
||||
|
144
crates/jet-api/src/http/api/authentication/sign_in.rs
Normal file
144
crates/jet-api/src/http/api/authentication/sign_in.rs
Normal file
@ -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-in")]
|
||||
pub async fn sign_in(
|
||||
_: RequireInstanceConfigured,
|
||||
req: HttpRequest,
|
||||
payload: Json<SignInPayload>,
|
||||
db: Data<DatabaseConnection>,
|
||||
config: Data<ApplicationConfig>,
|
||||
event_bus: Data<crate::EventBusClient>,
|
||||
session: Data<SessionStorage>,
|
||||
) -> Result<HttpResponse, JsonError> {
|
||||
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<SignInPayload>,
|
||||
db: &mut DatabaseTransaction,
|
||||
config: Data<ApplicationConfig>,
|
||||
event_bus: Data<crate::EventBusClient>,
|
||||
session: Data<SessionStorage>,
|
||||
) -> Result<HttpResponse, JsonError> {
|
||||
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<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();
|
||||
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 SignInPayload {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
@ -1,30 +1,14 @@
|
||||
use std::env::var as env_var;
|
||||
|
||||
use super::AuthError;
|
||||
use super::{auth_http_response, AuthError, AuthResponseBody};
|
||||
use actix_jwt_session::{JwtTtl, RefreshTtl, JWT_HEADER_NAME, REFRESH_HEADER_NAME};
|
||||
use actix_web::{
|
||||
get,
|
||||
web::{self, Data, ServiceConfig},
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use entities::project_member_invites::{
|
||||
Column as ProjectMemberInviteColumn, Model as ProjectMemberInvite,
|
||||
};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::{ActiveModel as UserModel, Column as UserColumn};
|
||||
use entities::workspace_member_invites::{
|
||||
Column as WorkspaceMemberInviteColumn, Model as WorkspaceMemberInvite,
|
||||
};
|
||||
use entities::workspace_members::ActiveModel as WorkspaceMemberModel;
|
||||
use entities::{
|
||||
prelude::{
|
||||
ProjectMemberInvites, ProjectMembers, Users, WorkspaceMemberInvites, WorkspaceMembers,
|
||||
},
|
||||
sea_orm_active_enums::Roles,
|
||||
};
|
||||
use entities::{
|
||||
project_members::ActiveModel as ProjectMemberModel, sea_orm_active_enums::ProjectMemberRoles,
|
||||
};
|
||||
use http_api_isahc_client::IsahcClient;
|
||||
use jet_contract::{
|
||||
event_bus::{Msg, SignInMedium, Topic, UserMsg},
|
||||
@ -43,10 +27,10 @@ use oauth2_google::{
|
||||
GoogleProviderForWebServerAppsAccessType, GoogleScope,
|
||||
};
|
||||
use oauth2_signin::web_app::{SigninFlow, SigninFlowHandleCallbackByQueryConfiguration};
|
||||
use reqwest::header::{LOCATION, USER_AGENT};
|
||||
use reqwest::{header::LOCATION, StatusCode};
|
||||
use sea_orm::{
|
||||
sea_query::OnConflict, ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection,
|
||||
DatabaseTransaction, EntityTrait, QueryFilter, Set, TransactionTrait,
|
||||
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection, DatabaseTransaction,
|
||||
EntityTrait, QueryFilter, Set, TransactionTrait,
|
||||
};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
@ -236,7 +220,7 @@ async fn handle_user_info(
|
||||
user_info: UserInfo,
|
||||
event_bus: Data<crate::EventBusClient>,
|
||||
session: Data<actix_jwt_session::SessionStorage>,
|
||||
) -> std::result::Result<HttpResponse, AuthError> {
|
||||
) -> std::result::Result<HttpResponse, Error> {
|
||||
let UserInfo {
|
||||
uid: _,
|
||||
name: _,
|
||||
@ -273,18 +257,7 @@ async fn handle_user_info(
|
||||
}
|
||||
};
|
||||
|
||||
let ip = req
|
||||
.peer_addr()
|
||||
.ok_or(AuthError::NoPeerAddr)?
|
||||
.ip()
|
||||
.to_string();
|
||||
let user_agent = req
|
||||
.headers()
|
||||
.get(USER_AGENT)
|
||||
.ok_or(AuthError::NoUserAgent)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidUserAgent)?
|
||||
.to_string();
|
||||
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()));
|
||||
@ -303,144 +276,7 @@ async fn handle_user_info(
|
||||
}
|
||||
};
|
||||
|
||||
let workspace_invites = WorkspaceMemberInvites::find()
|
||||
.filter(
|
||||
WorkspaceMemberInviteColumn::Accepted
|
||||
.eq(true)
|
||||
.and(WorkspaceMemberInviteColumn::Email.eq(&email)),
|
||||
)
|
||||
.all(&mut *db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to update user {user_id:?} on oauth {provider}: {e}");
|
||||
OAuthError::FetchWorkspaceInvites {
|
||||
user_id,
|
||||
provider: provider.to_owned(),
|
||||
}
|
||||
})?;
|
||||
let project_invites = ProjectMemberInvites::find()
|
||||
.filter(
|
||||
ProjectMemberInviteColumn::Accepted
|
||||
.eq(true)
|
||||
.and(ProjectMemberInviteColumn::Email.eq(&email)),
|
||||
)
|
||||
.all(&mut *db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to update user {user_id:?} on oauth {provider}: {e}");
|
||||
OAuthError::FetchProjectInvites {
|
||||
user_id,
|
||||
provider: provider.to_owned(),
|
||||
}
|
||||
})?;
|
||||
|
||||
// Create workspace members
|
||||
let insert = WorkspaceMembers::insert_many(workspace_invites.iter().map(
|
||||
|WorkspaceMemberInvite {
|
||||
role,
|
||||
created_by_id,
|
||||
workspace_id,
|
||||
..
|
||||
}| WorkspaceMemberModel {
|
||||
id: NotSet,
|
||||
created_at: Set(Utc::now().fixed_offset()),
|
||||
updated_at: Set(Utc::now().fixed_offset()),
|
||||
role: Set(match role {
|
||||
Roles::Admin => Roles::Member,
|
||||
_ => role.clone(),
|
||||
}),
|
||||
created_by_id: Set(created_by_id.clone()),
|
||||
member_id: Set(user_id),
|
||||
updated_by_id: NotSet,
|
||||
workspace_id: Set(workspace_id.clone()),
|
||||
company_role: NotSet,
|
||||
view_props: NotSet,
|
||||
default_props: NotSet,
|
||||
issue_props: NotSet,
|
||||
is_active: Set(true),
|
||||
},
|
||||
));
|
||||
if let Err(e) = insert
|
||||
.on_conflict(OnConflict::new().do_nothing().to_owned())
|
||||
.exec(&mut *db)
|
||||
.await
|
||||
{
|
||||
error!("Failed to add user {user_id:?} to workspace with provider {provider}: {e}");
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json(JsonError::new("Failed to add user to workspaces")));
|
||||
}
|
||||
|
||||
// Create project members
|
||||
let insert = ProjectMembers::insert_many(project_invites.iter().map(
|
||||
|ProjectMemberInvite {
|
||||
role,
|
||||
created_by_id,
|
||||
workspace_id,
|
||||
..
|
||||
}| ProjectMemberModel {
|
||||
id: NotSet,
|
||||
created_at: Set(Utc::now().fixed_offset()),
|
||||
updated_at: Set(Utc::now().fixed_offset()),
|
||||
role: Set(match role {
|
||||
ProjectMemberRoles::Admin => ProjectMemberRoles::Member,
|
||||
_ => role.clone(),
|
||||
}),
|
||||
created_by_id: Set(created_by_id.clone()),
|
||||
member_id: Set(Some(user_id)),
|
||||
updated_by_id: NotSet,
|
||||
workspace_id: Set(workspace_id.clone()),
|
||||
view_props: NotSet,
|
||||
default_props: NotSet,
|
||||
is_active: Set(true),
|
||||
comment: Set(None),
|
||||
project_id: NotSet,
|
||||
sort_order: NotSet,
|
||||
preferences: NotSet,
|
||||
},
|
||||
));
|
||||
if let Err(e) = insert
|
||||
.on_conflict(OnConflict::new().do_nothing().to_owned())
|
||||
.exec(&mut *db)
|
||||
.await
|
||||
{
|
||||
error!("Failed to add user {user_id:?} to project with provider {provider}: {e}");
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json(JsonError::new("Failed to add user to projects")));
|
||||
}
|
||||
|
||||
// cleanups
|
||||
if let Err(e) = WorkspaceMemberInvites::delete_many()
|
||||
.filter(
|
||||
WorkspaceMemberInviteColumn::Id.is_in(
|
||||
workspace_invites
|
||||
.into_iter()
|
||||
.map(|w| uuid::Uuid::from_u128(w.id.as_u128()))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.exec(&mut *db)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed clean up workspace invites for user {user_id:?} with provider {provider}: {e}"
|
||||
);
|
||||
};
|
||||
if let Err(e) = ProjectMemberInvites::delete_many()
|
||||
.filter(
|
||||
ProjectMemberInviteColumn::Id.is_in(
|
||||
project_invites
|
||||
.into_iter()
|
||||
.map(|w| uuid::Uuid::from_u128(w.id.as_u128()))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.exec(&mut *db)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed clean up project invites for user {user_id:?} with provider {provider}: {e}"
|
||||
);
|
||||
};
|
||||
crate::utils::invites_to_membership(user_id, &email, Some(provider), &mut *db).await?;
|
||||
|
||||
entities::social_login_connections::ActiveModel {
|
||||
id: NotSet,
|
||||
@ -459,10 +295,10 @@ async fn handle_user_info(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to create social media connection {provider:?}: {e}");
|
||||
OAuthError::ConnectSocialMedia {
|
||||
AuthError::from(OAuthError::ConnectSocialMedia {
|
||||
user_id,
|
||||
provider: provider.to_owned(),
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Err(e) = event_bus
|
||||
@ -484,45 +320,7 @@ async fn handle_user_info(
|
||||
warn!("Failed to publish sign-in msg after {provider} callback: {e}");
|
||||
};
|
||||
|
||||
let access_ttl = JwtTtl(actix_jwt_session::Duration::days(9_999));
|
||||
let refresh_ttl = RefreshTtl(actix_jwt_session::Duration::days(9_999));
|
||||
let claims = AppClaims::from_user(user, access_ttl);
|
||||
let pair = match session.store(claims, access_ttl, refresh_ttl).await {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
error!("Failed to store session: {e}");
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
};
|
||||
|
||||
let access_token = match pair.jwt.encode() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to store session: {e}");
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
};
|
||||
let refresh_token = match pair.refresh.encode() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to store session: {e}");
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct Response {
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Created()
|
||||
.append_header((JWT_HEADER_NAME, access_token.clone()))
|
||||
.append_header((REFRESH_HEADER_NAME, refresh_token.clone()))
|
||||
.json(Response {
|
||||
access_token,
|
||||
refresh_token,
|
||||
}))
|
||||
auth_http_response(user, session, StatusCode::CREATED).await
|
||||
}
|
||||
|
||||
fn github_flow(
|
||||
|
@ -1,5 +1,4 @@
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_jwt_session::*;
|
||||
use actix_web::{web::Data, App, HttpServer};
|
||||
@ -9,9 +8,11 @@ pub use sea_orm::{Database, DatabaseConnection};
|
||||
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
pub mod extractors;
|
||||
pub mod http;
|
||||
pub mod models;
|
||||
pub mod session;
|
||||
pub mod utils;
|
||||
|
||||
pub const APPLICATION_NAME: &str = "jet-api";
|
||||
|
||||
@ -40,21 +41,16 @@ async fn main() {
|
||||
)
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
let keys = JwtSigningKeys::load_or_create();
|
||||
let (storage, factory) = SessionMiddlewareFactory::<session::AppClaims>::build(
|
||||
Arc::new(keys.encoding_key),
|
||||
Arc::new(keys.decoding_key),
|
||||
Algorithm::EdDSA,
|
||||
)
|
||||
.with_redis_pool(redis.clone())
|
||||
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
||||
.with_jwt_header(JWT_HEADER_NAME)
|
||||
// Check if cookie JWT exists and contains encoded JWT
|
||||
.with_jwt_cookie(JWT_COOKIE_NAME)
|
||||
.with_refresh_header(REFRESH_HEADER_NAME)
|
||||
// Check if cookie JWT exists and contains encoded JWT
|
||||
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
||||
.finish();
|
||||
let (storage, factory) = SessionMiddlewareFactory::<session::AppClaims>::build_ed_dsa()
|
||||
.with_redis_pool(redis.clone())
|
||||
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
||||
.with_jwt_header(JWT_HEADER_NAME)
|
||||
// Check if cookie JWT exists and contains encoded JWT
|
||||
.with_jwt_cookie(JWT_COOKIE_NAME)
|
||||
.with_refresh_header(REFRESH_HEADER_NAME)
|
||||
// Check if cookie JWT exists and contains encoded JWT
|
||||
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
||||
.finish();
|
||||
let jwt_ttl = JwtTtl(Duration::days(9999));
|
||||
let refresh_ttl = RefreshTtl(Duration::days(3 * 31 * 999));
|
||||
let (eb_client, eb_stream) = rumqttc::AsyncClient::new(
|
||||
@ -76,9 +72,7 @@ async fn main() {
|
||||
crate::events::handle_events(eb_stream).await;
|
||||
|
||||
let http_client = reqwest::Client::new();
|
||||
let redis = Data::new(redis);
|
||||
let application_config = Data::new(application_config);
|
||||
let db = Data::new(db);
|
||||
let event_bus = Data::new(jet_contract::event_bus::Client::new(eb_client));
|
||||
|
||||
HttpServer::new(move || {
|
||||
@ -86,9 +80,9 @@ async fn main() {
|
||||
.app_data(Data::new(storage.clone()))
|
||||
.app_data(Data::new(jwt_ttl))
|
||||
.app_data(Data::new(refresh_ttl))
|
||||
.app_data(Data::new(redis.clone()))
|
||||
.app_data(Data::new(db.clone()))
|
||||
.app_data(application_config.clone())
|
||||
.app_data(redis.clone())
|
||||
.app_data(db.clone())
|
||||
.app_data(event_bus.clone())
|
||||
.wrap(factory.clone())
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
|
@ -1,9 +1,11 @@
|
||||
use actix_web::HttpResponse;
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::http::AuthError;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Display)]
|
||||
#[display(fmt = "{{\"error\":\"{error}\"}}")]
|
||||
pub struct JsonError {
|
||||
pub error: String,
|
||||
}
|
||||
@ -16,6 +18,18 @@ impl JsonError {
|
||||
}
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for JsonError {
|
||||
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
|
||||
HttpResponse::BadRequest().json(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for JsonError {
|
||||
fn from(e: Error) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display)]
|
||||
pub enum Error {
|
||||
#[display(fmt = "Database connection error")]
|
||||
@ -30,6 +44,10 @@ pub enum Error {
|
||||
Auth(AuthError),
|
||||
#[display(fmt = "Internal server error, failed to connect to storage")]
|
||||
RedisConnection,
|
||||
#[display(fmt = "Failed to add user to workspaces")]
|
||||
AddToWorkspace,
|
||||
#[display(fmt = "Failed to add user to projects")]
|
||||
AddToProject,
|
||||
}
|
||||
|
||||
impl From<AuthError> for Error {
|
||||
|
185
crates/jet-api/src/utils/mod.rs
Normal file
185
crates/jet-api/src/utils/mod.rs
Normal file
@ -0,0 +1,185 @@
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use chrono::Utc;
|
||||
use entities::project_member_invites::{
|
||||
Column as ProjectMemberInviteColumn, Model as ProjectMemberInvite,
|
||||
};
|
||||
use entities::workspace_member_invites::{
|
||||
Column as WorkspaceMemberInviteColumn, Model as WorkspaceMemberInvite,
|
||||
};
|
||||
use entities::workspace_members::ActiveModel as WorkspaceMemberModel;
|
||||
use entities::{
|
||||
prelude::{ProjectMemberInvites, ProjectMembers, WorkspaceMemberInvites, WorkspaceMembers},
|
||||
sea_orm_active_enums::Roles,
|
||||
};
|
||||
use entities::{
|
||||
project_members::ActiveModel as ProjectMemberModel, sea_orm_active_enums::ProjectMemberRoles,
|
||||
};
|
||||
use reqwest::header::USER_AGENT;
|
||||
use sea_orm::sea_query::OnConflict;
|
||||
use sea_orm::*;
|
||||
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> {
|
||||
let ip = req.peer_addr().ok_or(AuthError::NoPeerAddr)?.ip();
|
||||
let user_agent = req
|
||||
.headers()
|
||||
.get(USER_AGENT)
|
||||
.ok_or(AuthError::NoUserAgent)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidUserAgent)?
|
||||
.to_string();
|
||||
let current_site = req.uri().host().ok_or(Error::NoHost)?.to_owned();
|
||||
Ok((ip.to_string(), user_agent, current_site))
|
||||
}
|
||||
|
||||
pub async fn invites_to_membership(
|
||||
user_id: Uuid,
|
||||
email: &str,
|
||||
provider: Option<&str>,
|
||||
db: &mut DatabaseTransaction,
|
||||
) -> Result<(), Error> {
|
||||
let workspace_invites = WorkspaceMemberInvites::find()
|
||||
.filter(
|
||||
WorkspaceMemberInviteColumn::Accepted
|
||||
.eq(true)
|
||||
.and(WorkspaceMemberInviteColumn::Email.eq(email)),
|
||||
)
|
||||
.all(&mut *db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to update user {user_id:?} on oauth {provider:?}: {e}");
|
||||
AuthError::from(OAuthError::FetchWorkspaceInvites {
|
||||
user_id,
|
||||
provider: provider.to_owned().unwrap_or("--NONE--").to_owned(),
|
||||
})
|
||||
})?;
|
||||
let project_invites = ProjectMemberInvites::find()
|
||||
.filter(
|
||||
ProjectMemberInviteColumn::Accepted
|
||||
.eq(true)
|
||||
.and(ProjectMemberInviteColumn::Email.eq(email)),
|
||||
)
|
||||
.all(&mut *db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to update user {user_id:?} on oauth {provider:?}: {e}");
|
||||
AuthError::from(OAuthError::FetchProjectInvites {
|
||||
user_id,
|
||||
provider: provider.to_owned().unwrap_or("--NONE--").to_owned(),
|
||||
})
|
||||
})?;
|
||||
|
||||
// Create workspace members
|
||||
let insert = WorkspaceMembers::insert_many(workspace_invites.iter().map(
|
||||
|WorkspaceMemberInvite {
|
||||
role,
|
||||
created_by_id,
|
||||
workspace_id,
|
||||
..
|
||||
}| WorkspaceMemberModel {
|
||||
id: NotSet,
|
||||
created_at: Set(Utc::now().fixed_offset()),
|
||||
updated_at: Set(Utc::now().fixed_offset()),
|
||||
role: Set(match role {
|
||||
Roles::Admin => Roles::Member,
|
||||
_ => role.clone(),
|
||||
}),
|
||||
created_by_id: Set(created_by_id.clone()),
|
||||
member_id: Set(user_id),
|
||||
updated_by_id: NotSet,
|
||||
workspace_id: Set(workspace_id.clone()),
|
||||
company_role: NotSet,
|
||||
view_props: NotSet,
|
||||
default_props: NotSet,
|
||||
issue_props: NotSet,
|
||||
is_active: Set(true),
|
||||
},
|
||||
));
|
||||
if let Err(e) = insert
|
||||
.on_conflict(OnConflict::new().do_nothing().to_owned())
|
||||
.exec(&mut *db)
|
||||
.await
|
||||
{
|
||||
error!("Failed to add user {user_id:?} to workspace with provider {provider:?}: {e}");
|
||||
return Err(Error::AddToWorkspace);
|
||||
}
|
||||
|
||||
// Create project members
|
||||
let insert = ProjectMembers::insert_many(project_invites.iter().map(
|
||||
|ProjectMemberInvite {
|
||||
role,
|
||||
created_by_id,
|
||||
workspace_id,
|
||||
..
|
||||
}| ProjectMemberModel {
|
||||
id: NotSet,
|
||||
created_at: Set(Utc::now().fixed_offset()),
|
||||
updated_at: Set(Utc::now().fixed_offset()),
|
||||
role: Set(match role {
|
||||
ProjectMemberRoles::Admin => ProjectMemberRoles::Member,
|
||||
_ => role.clone(),
|
||||
}),
|
||||
created_by_id: Set(created_by_id.clone()),
|
||||
member_id: Set(Some(user_id)),
|
||||
updated_by_id: NotSet,
|
||||
workspace_id: Set(workspace_id.clone()),
|
||||
view_props: NotSet,
|
||||
default_props: NotSet,
|
||||
is_active: Set(true),
|
||||
comment: Set(None),
|
||||
project_id: NotSet,
|
||||
sort_order: NotSet,
|
||||
preferences: NotSet,
|
||||
},
|
||||
));
|
||||
if let Err(e) = insert
|
||||
.on_conflict(OnConflict::new().do_nothing().to_owned())
|
||||
.exec(&mut *db)
|
||||
.await
|
||||
{
|
||||
error!("Failed to add user {user_id:?} to project with provider {provider:?}: {e}");
|
||||
return Err(Error::AddToProject);
|
||||
}
|
||||
|
||||
// cleanups
|
||||
if let Err(e) = WorkspaceMemberInvites::delete_many()
|
||||
.filter(
|
||||
WorkspaceMemberInviteColumn::Id.is_in(
|
||||
workspace_invites
|
||||
.into_iter()
|
||||
.map(|w| uuid::Uuid::from_u128(w.id.as_u128()))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.exec(&mut *db)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed clean up workspace invites for user {user_id:?} with provider {provider:?}: {e}"
|
||||
);
|
||||
return Err(Error::DatabaseError);
|
||||
};
|
||||
if let Err(e) = ProjectMemberInvites::delete_many()
|
||||
.filter(
|
||||
ProjectMemberInviteColumn::Id.is_in(
|
||||
project_invites
|
||||
.into_iter()
|
||||
.map(|w| uuid::Uuid::from_u128(w.id.as_u128()))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.exec(&mut *db)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed clean up project invites for user {user_id:?} with provider {provider:?}: {e}"
|
||||
);
|
||||
return Err(Error::DatabaseError);
|
||||
};
|
||||
Ok(())
|
||||
}
|
@ -58,6 +58,8 @@ pub enum SignInMedium {
|
||||
MagicLink,
|
||||
#[display(fmt = "oauth")]
|
||||
OAuth,
|
||||
#[display(fmt = "EMAIL")]
|
||||
Email,
|
||||
}
|
||||
|
||||
impl SignInMedium {
|
||||
@ -65,6 +67,7 @@ impl SignInMedium {
|
||||
match self {
|
||||
Self::MagicLink => "MAGIC_LINK",
|
||||
Self::OAuth => "oauth",
|
||||
Self::Email => "EMAIL",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user