OAuth callbacks
This commit is contained in:
parent
56ef7c9318
commit
0923427551
65
Cargo.lock
generated
65
Cargo.lock
generated
@ -86,9 +86,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-jwt-session"
|
name = "actix-jwt-session"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "370a6266b24b472629abdbd222d9ea735c0ac86c81fa2a3851dee86c82d99521"
|
checksum = "6317d3303618eea36d68898bf720ce94d8b5bb2bf1a23c8ffdf714024f0a49a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"argon2",
|
"argon2",
|
||||||
@ -97,6 +97,7 @@ dependencies = [
|
|||||||
"cookie 0.17.0",
|
"cookie 0.17.0",
|
||||||
"deadpool",
|
"deadpool",
|
||||||
"deadpool-redis",
|
"deadpool-redis",
|
||||||
|
"derive_more",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -368,9 +369,9 @@ checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.2"
|
version = "0.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9"
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64ct",
|
"base64ct",
|
||||||
"blake2",
|
"blake2",
|
||||||
@ -687,9 +688,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.14.0"
|
version = "1.14.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@ -742,9 +743,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.31"
|
version = "0.4.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
@ -753,7 +754,7 @@ dependencies = [
|
|||||||
"pure-rust-locales",
|
"pure-rust-locales",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1075,6 +1076,12 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
|
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
@ -1757,7 +1764,9 @@ dependencies = [
|
|||||||
"actix-web",
|
"actix-web",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"chrono",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
"dotenv",
|
||||||
"entities",
|
"entities",
|
||||||
"figment",
|
"figment",
|
||||||
"futures",
|
"futures",
|
||||||
@ -2320,9 +2329,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.62"
|
version = "0.10.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671"
|
checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.2",
|
"bitflags 2.4.2",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@ -2352,9 +2361,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.98"
|
version = "0.9.99"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7"
|
checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@ -2626,9 +2635,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.76"
|
version = "1.0.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@ -2808,13 +2817,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.2"
|
version = "1.10.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata 0.4.3",
|
"regex-automata 0.4.5",
|
||||||
"regex-syntax 0.8.2",
|
"regex-syntax 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2829,9 +2838,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -3224,9 +3233,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-orm"
|
name = "sea-orm"
|
||||||
version = "0.12.11"
|
version = "0.12.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59f66e2129991acd51fcad7b59da1edd862edca69773cc9a19cb17b967fae2fb"
|
checksum = "0cbf88748872fa54192476d6d49d0775e208566a72656e267e45f6980b926c8d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -3252,9 +3261,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-orm-macros"
|
name = "sea-orm-macros"
|
||||||
version = "0.12.11"
|
version = "0.12.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b03da1864306242678ac3b6567e69f70dd1252a72742baa23a4d92d2d45da3fc"
|
checksum = "e0dbc880d47aa53c6a572e39c99402c7fad59b50766e51e0b0fc1306510b0555"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -3553,9 +3562,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.0"
|
version = "1.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b187f0231d56fe41bfb12034819dd2bf336422a5866de41bc3fec4b2e3883e8"
|
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smartstring"
|
name = "smartstring"
|
||||||
@ -4280,9 +4289,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uncased"
|
name = "uncased"
|
||||||
version = "0.9.9"
|
version = "0.9.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68"
|
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
||||||
|
|
||||||
use super::sea_orm_active_enums::ProjectMemberInviteRoles;
|
use super::sea_orm_active_enums::ProjectMemberRoles;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ pub struct Model {
|
|||||||
#[sea_orm(column_type = "Text", nullable)]
|
#[sea_orm(column_type = "Text", nullable)]
|
||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
pub responded_at: Option<DateTimeWithTimeZone>,
|
pub responded_at: Option<DateTimeWithTimeZone>,
|
||||||
pub role: ProjectMemberInviteRoles,
|
pub role: ProjectMemberRoles,
|
||||||
pub created_by_id: Option<Uuid>,
|
pub created_by_id: Option<Uuid>,
|
||||||
pub project_id: Uuid,
|
pub project_id: Uuid,
|
||||||
pub updated_by_id: Option<Uuid>,
|
pub updated_by_id: Option<Uuid>,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
||||||
|
|
||||||
use super::sea_orm_active_enums::Roles;
|
use super::sea_orm_active_enums::ProjectMemberRoles;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ pub struct Model {
|
|||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
#[sea_orm(column_type = "Text", nullable)]
|
#[sea_orm(column_type = "Text", nullable)]
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
pub role: Roles,
|
pub role: ProjectMemberRoles,
|
||||||
pub created_by_id: Option<Uuid>,
|
pub created_by_id: Option<Uuid>,
|
||||||
pub member_id: Option<Uuid>,
|
pub member_id: Option<Uuid>,
|
||||||
pub project_id: Uuid,
|
pub project_id: Uuid,
|
||||||
|
@ -7,9 +7,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
db_type = "Enum",
|
||||||
enum_name = "project_member_invite_roles"
|
enum_name = "project_member_roles"
|
||||||
)]
|
)]
|
||||||
pub enum ProjectMemberInviteRoles {
|
pub enum ProjectMemberRoles {
|
||||||
#[sea_orm(string_value = "Admin")]
|
#[sea_orm(string_value = "Admin")]
|
||||||
Admin,
|
Admin,
|
||||||
#[sea_orm(string_value = "Guest")]
|
#[sea_orm(string_value = "Guest")]
|
||||||
|
@ -14,7 +14,7 @@ rumqttc = { version = "0.23.0", features = ["use-rustls"] }
|
|||||||
rust-s3 = { version = "0.33.0", features = ["tokio-rustls-tls", "futures-util", "futures-io"] }
|
rust-s3 = { version = "0.33.0", features = ["tokio-rustls-tls", "futures-util", "futures-io"] }
|
||||||
sea-orm = { version = "0.12.11", features = ["postgres-array", "sqlx-all"] }
|
sea-orm = { version = "0.12.11", features = ["postgres-array", "sqlx-all"] }
|
||||||
serde = "1.0.195"
|
serde = "1.0.195"
|
||||||
serde_json = "1.0.111"
|
serde_json = { version = "1.0.111", features = ["raw_value", "alloc"] }
|
||||||
tokio = { version = "1.35.1", features = ["full"] }
|
tokio = { version = "1.35.1", features = ["full"] }
|
||||||
jet-contract = { workspace = true }
|
jet-contract = { workspace = true }
|
||||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||||
@ -39,3 +39,5 @@ oauth2-signin = "0.2.0"
|
|||||||
oauth2-core = "0.2.0"
|
oauth2-core = "0.2.0"
|
||||||
reqwest = { version = "0.11.23", default-features = false, features = ["rustls", "tokio-rustls", "tokio-socks", "multipart"] }
|
reqwest = { version = "0.11.23", default-features = false, features = ["rustls", "tokio-rustls", "tokio-socks", "multipart"] }
|
||||||
http-api-isahc-client = { version = "0.2.2", features = ["with-sleep-via-tokio"] }
|
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"] }
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
use actix_web::web::{Data, ServiceConfig};
|
use actix_web::web::{Data, ServiceConfig};
|
||||||
use actix_web::{delete, get, HttpResponse};
|
use actix_web::{delete, get, HttpResponse};
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
mod email_check;
|
mod email_check;
|
||||||
|
mod social_auth;
|
||||||
|
|
||||||
pub fn configure(config: &mut ServiceConfig) {
|
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);
|
||||||
|
social_auth::configure(http_client, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, derive_more::Display)]
|
#[derive(Debug, Clone, Copy, derive_more::Display)]
|
||||||
@ -18,7 +21,17 @@ pub enum PublishError {
|
|||||||
MagicLinkEmail,
|
MagicLinkEmail,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, derive_more::Display, derive_more::From)]
|
#[derive(Debug, Clone, derive_more::Display)]
|
||||||
|
pub enum OAuthError {
|
||||||
|
#[display(fmt = "Failed to load workspace invides for {user_id} on {provider} callback")]
|
||||||
|
FetchWorkspaceInvites { user_id: Uuid, provider: String },
|
||||||
|
#[display(fmt = "Failed to load project invides for {user_id} on {provider} callback")]
|
||||||
|
FetchProjectInvites { user_id: Uuid, provider: String },
|
||||||
|
#[display(fmt = "Failed to connect user {user_id} to {provider}")]
|
||||||
|
ConnectSocialMedia { user_id: Uuid, provider: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::Display, derive_more::From)]
|
||||||
pub enum AuthError {
|
pub enum AuthError {
|
||||||
#[display(fmt = "New account creation is disabled. Please contact your site administrator")]
|
#[display(fmt = "New account creation is disabled. Please contact your site administrator")]
|
||||||
RegisterOff,
|
RegisterOff,
|
||||||
@ -37,6 +50,9 @@ pub enum AuthError {
|
|||||||
#[display(fmt = "{}", _0)]
|
#[display(fmt = "{}", _0)]
|
||||||
#[from]
|
#[from]
|
||||||
Publish(PublishError),
|
Publish(PublishError),
|
||||||
|
#[display(fmt = "{}", _0)]
|
||||||
|
#[from]
|
||||||
|
Oauth(OAuthError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/social-auth")]
|
#[get("/social-auth")]
|
||||||
|
623
crates/jet-api/src/http/api/authentication/social_auth.rs
Normal file
623
crates/jet-api/src/http/api/authentication/social_auth.rs
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
use std::env::var as env_var;
|
||||||
|
|
||||||
|
use super::AuthError;
|
||||||
|
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::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},
|
||||||
|
UserId,
|
||||||
|
};
|
||||||
|
use oauth2_amazon::{
|
||||||
|
AmazonExtensionsBuilder, AmazonProviderWithWebServices, AmazonScope, AmazonTokenUrlRegion,
|
||||||
|
};
|
||||||
|
use oauth2_client::extensions::UserInfo;
|
||||||
|
use oauth2_github::{GithubExtensionsBuilder, GithubProviderWithWebApplication, GithubScope};
|
||||||
|
use oauth2_gitlab::{
|
||||||
|
GitlabExtensionsBuilder, GitlabProviderForEndUsers, GitlabScope, BASE_URL_GITLAB_COM,
|
||||||
|
};
|
||||||
|
use oauth2_google::{
|
||||||
|
GoogleExtensionsBuilder, GoogleProviderForWebServerApps,
|
||||||
|
GoogleProviderForWebServerAppsAccessType, GoogleScope,
|
||||||
|
};
|
||||||
|
use oauth2_signin::web_app::{SigninFlow, SigninFlowHandleCallbackByQueryConfiguration};
|
||||||
|
use reqwest::header::{LOCATION, USER_AGENT};
|
||||||
|
use sea_orm::{
|
||||||
|
sea_query::OnConflict, ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection,
|
||||||
|
DatabaseTransaction, EntityTrait, QueryFilter, Set, TransactionTrait,
|
||||||
|
};
|
||||||
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
http::OAuthError,
|
||||||
|
models::{Error, JsonError},
|
||||||
|
session::AppClaims,
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! oauth_envs {
|
||||||
|
($env: expr, 2) => {{
|
||||||
|
use std::concat;
|
||||||
|
use std::env::var;
|
||||||
|
var(concat!($env, "_CLIENT_ID"))
|
||||||
|
.ok()
|
||||||
|
.zip(var(concat!($env, "_CLIENT_SECRET")).ok())
|
||||||
|
}};
|
||||||
|
($env: expr, 1) => {{
|
||||||
|
use std::concat;
|
||||||
|
use std::env::var;
|
||||||
|
var(concat!($env, "_CLIENT_ID")).ok()
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
macro_rules! init_flow_service {
|
||||||
|
($provider: expr, $flow: expr) => {
|
||||||
|
web::resource(std::concat!("/auth/", $provider)).get({
|
||||||
|
let flow = $flow.clone();
|
||||||
|
move || {
|
||||||
|
let flow = flow.clone();
|
||||||
|
async move {
|
||||||
|
let flow = flow.clone();
|
||||||
|
let Ok(uri) = flow.build_authorization_url(None) else {
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
};
|
||||||
|
HttpResponse::SeeOther()
|
||||||
|
.append_header((LOCATION, uri.as_str()))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! flow_callback {
|
||||||
|
($provider: expr, $flow: expr) => {{
|
||||||
|
web::resource(std::concat!("/auth/", $provider, "/callback")).get(
|
||||||
|
move |req: HttpRequest,
|
||||||
|
db: Data<DatabaseConnection>,
|
||||||
|
event_bus: Data<crate::EventBusClient>,
|
||||||
|
session: Data<actix_jwt_session::SessionStorage>| {
|
||||||
|
let flow = $flow.clone();
|
||||||
|
async move {
|
||||||
|
let res: Result<HttpResponse, Error> = handle_callback(
|
||||||
|
$provider,
|
||||||
|
req,
|
||||||
|
db,
|
||||||
|
flow.clone(),
|
||||||
|
event_bus.clone(),
|
||||||
|
session.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(_http_client: reqwest::Client, config: &mut ServiceConfig) {
|
||||||
|
let schema = env_var("JET_API_SCHEMA").unwrap();
|
||||||
|
let host = env_var("JET_API_HOST").unwrap();
|
||||||
|
let uri = format!("{schema}://{host}");
|
||||||
|
let client = http_api_isahc_client::IsahcClient::new().unwrap();
|
||||||
|
|
||||||
|
if let Some((client_id, client_secret)) = oauth_envs!("GITHUB", 2) {
|
||||||
|
let flow = Data::new(github_flow(client.clone(), client_id, client_secret, &uri));
|
||||||
|
config
|
||||||
|
.service(init_flow_service!("github", flow))
|
||||||
|
.service(flow_callback!("github", flow));
|
||||||
|
}
|
||||||
|
if let Some((client_id, client_secret)) = oauth_envs!("GITLAB", 2) {
|
||||||
|
let flow = Data::new(gitlab_flow(client.clone(), client_id, client_secret, &uri));
|
||||||
|
config
|
||||||
|
.service(init_flow_service!("gitlab", flow))
|
||||||
|
.service(flow_callback!("gitlab", flow));
|
||||||
|
}
|
||||||
|
if let Some((client_id, client_secret)) = oauth_envs!("GOOGLE", 2) {
|
||||||
|
let flow = Data::new(google_flow(client.clone(), client_id, client_secret, &uri));
|
||||||
|
config
|
||||||
|
.service(init_flow_service!("google", flow))
|
||||||
|
.service(flow_callback!("google", flow));
|
||||||
|
}
|
||||||
|
if let Some((client_id, client_secret)) = oauth_envs!("AMAZON", 2) {
|
||||||
|
let flow = Data::new(amazon_flow(client.clone(), client_id, client_secret, &uri));
|
||||||
|
config
|
||||||
|
.service(init_flow_service!("amazon", flow))
|
||||||
|
.service(flow_callback!("amazon", flow));
|
||||||
|
}
|
||||||
|
|
||||||
|
config.service(social_auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/social-auth")]
|
||||||
|
async fn social_auth() -> HttpResponse {
|
||||||
|
HttpResponse::NotImplemented().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_callback(
|
||||||
|
provider: &str,
|
||||||
|
req: HttpRequest,
|
||||||
|
db: Data<DatabaseConnection>,
|
||||||
|
flow: Data<SigninFlow<IsahcClient>>,
|
||||||
|
event_bus: Data<crate::EventBusClient>,
|
||||||
|
session: Data<actix_jwt_session::SessionStorage>,
|
||||||
|
) -> std::result::Result<HttpResponse, Error> {
|
||||||
|
debug!("Starting github callback");
|
||||||
|
let query = req.query_string();
|
||||||
|
let config = SigninFlowHandleCallbackByQueryConfiguration::new();
|
||||||
|
let ret = flow.handle_callback_by_query(query, config).await;
|
||||||
|
|
||||||
|
use oauth2_signin::web_app::SigninFlowHandleCallbackRet as R;
|
||||||
|
let response = match ret {
|
||||||
|
R::Ok((access_token_body, user_info)) => {
|
||||||
|
let mut tx = db.begin().await.map_err(|_| Error::DatabaseError)?;
|
||||||
|
match handle_user_info(
|
||||||
|
provider,
|
||||||
|
req,
|
||||||
|
&mut tx,
|
||||||
|
access_token_body.id_token,
|
||||||
|
user_info,
|
||||||
|
event_bus,
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(v) => {
|
||||||
|
tx.commit().await.map_err(|e| {
|
||||||
|
error!("Failed to commit social_auth changes to postgres: {e}");
|
||||||
|
Error::DatabaseError
|
||||||
|
})?;
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tx.rollback().await.map_err(|e| {
|
||||||
|
error!("Failed to rollback social_auth changes to postgres: {e}");
|
||||||
|
Error::DatabaseError
|
||||||
|
})?;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R::OkButUserInfoNone(access_token_body) => {
|
||||||
|
warn!("handle callback from {provider}");
|
||||||
|
let _id = access_token_body.id_token;
|
||||||
|
HttpResponse::FailedDependency().json(JsonError::new(
|
||||||
|
"Authentication provider did not return user info",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
R::OkButUserInfoObtainError((access_token_body, error)) => {
|
||||||
|
warn!("handle callback from {provider}: {error}");
|
||||||
|
let _id = access_token_body.id_token;
|
||||||
|
HttpResponse::FailedDependency().json(JsonError::new(
|
||||||
|
"Authentication provider refused to returns user info",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
R::OkButUserInfoEndpointExecuteError((access_token_body, error)) => {
|
||||||
|
warn!("handle callback from {provider}: {error}");
|
||||||
|
let _id = access_token_body.id_token;
|
||||||
|
HttpResponse::FailedDependency().json(JsonError::new(
|
||||||
|
"Authentication provider refused to returns user info",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
R::FlowHandleCallbackError(error) => {
|
||||||
|
warn!("handle callback from {provider} failed: {error}");
|
||||||
|
HttpResponse::FailedDependency()
|
||||||
|
.json(JsonError::new("Authentication provider returns an error"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_user_info(
|
||||||
|
provider: &str,
|
||||||
|
req: HttpRequest,
|
||||||
|
db: &mut DatabaseTransaction,
|
||||||
|
id_token: Option<String>,
|
||||||
|
user_info: UserInfo,
|
||||||
|
event_bus: Data<crate::EventBusClient>,
|
||||||
|
session: Data<actix_jwt_session::SessionStorage>,
|
||||||
|
) -> std::result::Result<HttpResponse, AuthError> {
|
||||||
|
let UserInfo {
|
||||||
|
uid: _,
|
||||||
|
name: _,
|
||||||
|
email,
|
||||||
|
raw: _,
|
||||||
|
} = user_info;
|
||||||
|
|
||||||
|
let Some(email) = email else {
|
||||||
|
return Ok(HttpResponse::BadRequest().json(JsonError::new(
|
||||||
|
"Something went wrong. Please try again later or contact the support team.",
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
if !email.contains('@') {
|
||||||
|
return Ok(HttpResponse::BadRequest().json(JsonError::new(
|
||||||
|
"Something went wrong. Please try again later or contact the support team.",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = Users::find()
|
||||||
|
.filter(UserColumn::Email.eq(&email))
|
||||||
|
.one(&mut *db)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut user: UserModel = match user {
|
||||||
|
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 = 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();
|
||||||
|
|
||||||
|
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.last_login_medium = Set(SignInMedium::OAuth.to_string());
|
||||||
|
user.is_email_verified = Set(true);
|
||||||
|
|
||||||
|
let user_id = user.id.clone().unwrap();
|
||||||
|
let user = match user.update(&mut *db).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to update user {user_id:?} on oauth {provider}: {e}");
|
||||||
|
return Ok(HttpResponse::InternalServerError().finish());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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}"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
entities::social_login_connections::ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
created_at: NotSet,
|
||||||
|
updated_at: NotSet,
|
||||||
|
medium: Set(provider.to_owned()),
|
||||||
|
last_login_at: Set(Some(chrono::Utc::now().fixed_offset())),
|
||||||
|
last_received_at: NotSet,
|
||||||
|
token_data: Set(Some(serde_json::json!({ "id_token": id_token }))),
|
||||||
|
extra_data: NotSet,
|
||||||
|
created_by_id: Set(Some(user_id)),
|
||||||
|
updated_by_id: NotSet,
|
||||||
|
user_id: Set(user_id),
|
||||||
|
}
|
||||||
|
.save(&mut *db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to create social media connection {provider:?}: {e}");
|
||||||
|
OAuthError::ConnectSocialMedia {
|
||||||
|
user_id,
|
||||||
|
provider: provider.to_owned(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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::OAuth,
|
||||||
|
first_time: false,
|
||||||
|
}),
|
||||||
|
rumqttc::QoS::AtLeastOnce,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn github_flow(
|
||||||
|
client: IsahcClient,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: String,
|
||||||
|
uri: &str,
|
||||||
|
) -> SigninFlow<IsahcClient> {
|
||||||
|
SigninFlow::new(
|
||||||
|
client.clone(),
|
||||||
|
GithubProviderWithWebApplication::new(
|
||||||
|
client_id.to_owned(),
|
||||||
|
client_secret.to_owned(),
|
||||||
|
format!("{uri}/api/auth/github/callback").parse().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
vec![GithubScope::PublicRepo, GithubScope::UserEmail],
|
||||||
|
GithubExtensionsBuilder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gitlab_flow(
|
||||||
|
client: IsahcClient,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: String,
|
||||||
|
uri: &str,
|
||||||
|
) -> SigninFlow<IsahcClient> {
|
||||||
|
SigninFlow::new(
|
||||||
|
client.clone(),
|
||||||
|
GitlabProviderForEndUsers::new(
|
||||||
|
BASE_URL_GITLAB_COM,
|
||||||
|
client_id.to_owned(),
|
||||||
|
client_secret.to_owned(),
|
||||||
|
oauth2_core::types::RedirectUri::Url(
|
||||||
|
format!("{uri}/api/auth/gitlab/callback").parse().unwrap(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
vec![
|
||||||
|
GitlabScope::Openid,
|
||||||
|
GitlabScope::Profile,
|
||||||
|
GitlabScope::Email,
|
||||||
|
GitlabScope::ReadUser,
|
||||||
|
],
|
||||||
|
GitlabExtensionsBuilder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn google_flow(
|
||||||
|
client: IsahcClient,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: String,
|
||||||
|
uri: &str,
|
||||||
|
) -> SigninFlow<IsahcClient> {
|
||||||
|
SigninFlow::new(
|
||||||
|
client.clone(),
|
||||||
|
GoogleProviderForWebServerApps::new(
|
||||||
|
client_id.clone(),
|
||||||
|
client_secret.clone(),
|
||||||
|
oauth2_core::types::RedirectUri::Url(
|
||||||
|
format!("{uri}/api/auth/google/callback").parse().unwrap(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.configure(|x| {
|
||||||
|
x.access_type = Some(GoogleProviderForWebServerAppsAccessType::Offline);
|
||||||
|
x.include_granted_scopes = Some(true);
|
||||||
|
}),
|
||||||
|
vec![
|
||||||
|
GoogleScope::Email,
|
||||||
|
GoogleScope::Profile,
|
||||||
|
GoogleScope::Openid,
|
||||||
|
],
|
||||||
|
GoogleExtensionsBuilder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn amazon_flow(
|
||||||
|
client: IsahcClient,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: String,
|
||||||
|
uri: &str,
|
||||||
|
) -> SigninFlow<IsahcClient> {
|
||||||
|
SigninFlow::new(
|
||||||
|
client,
|
||||||
|
AmazonProviderWithWebServices::new(
|
||||||
|
client_id.to_owned(),
|
||||||
|
client_secret.to_owned(),
|
||||||
|
oauth2_core::types::RedirectUri::Url(
|
||||||
|
format!("{uri}/api/auth/amazon/callback").parse().unwrap(),
|
||||||
|
),
|
||||||
|
AmazonTokenUrlRegion::NA,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
vec![AmazonScope::Profile, AmazonScope::PostalCode],
|
||||||
|
AmazonExtensionsBuilder,
|
||||||
|
)
|
||||||
|
}
|
@ -3,15 +3,13 @@ pub use authentication::*;
|
|||||||
|
|
||||||
mod authentication;
|
mod authentication;
|
||||||
mod config;
|
mod config;
|
||||||
mod social_auth;
|
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
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("/api")
|
scope("/api")
|
||||||
.configure(config::configure)
|
.configure(config::configure)
|
||||||
.configure(users::configure)
|
.configure(users::configure),
|
||||||
.configure(authentication::configure)
|
|
||||||
.configure(|c| social_auth::configure(http_client, c)),
|
|
||||||
);
|
);
|
||||||
|
authentication::configure(http_client, config);
|
||||||
}
|
}
|
||||||
|
@ -1,192 +0,0 @@
|
|||||||
use std::env::var as env_var;
|
|
||||||
|
|
||||||
use actix_web::{
|
|
||||||
get,
|
|
||||||
web::{Data, Query, ServiceConfig},
|
|
||||||
HttpResponse,
|
|
||||||
};
|
|
||||||
use derive_more::Deref;
|
|
||||||
use http_api_isahc_client::IsahcClient;
|
|
||||||
use oauth2_amazon::{
|
|
||||||
AmazonExtensionsBuilder, AmazonProviderWithWebServices, AmazonScope, AmazonTokenUrlRegion,
|
|
||||||
};
|
|
||||||
use oauth2_github::{GithubExtensionsBuilder, GithubProviderWithWebApplication, GithubScope};
|
|
||||||
use oauth2_gitlab::{
|
|
||||||
GitlabExtensionsBuilder, GitlabProviderForEndUsers, GitlabScope, BASE_URL_GITLAB_COM,
|
|
||||||
};
|
|
||||||
use oauth2_google::{
|
|
||||||
GoogleExtensionsBuilder, GoogleProviderForWebServerApps,
|
|
||||||
GoogleProviderForWebServerAppsAccessType, GoogleScope,
|
|
||||||
};
|
|
||||||
use oauth2_signin::web_app::{
|
|
||||||
SigninFlow,
|
|
||||||
SigninFlowHandleCallbackByQueryConfiguration,
|
|
||||||
};
|
|
||||||
use tracing::{debug, info};
|
|
||||||
|
|
||||||
macro_rules! oauth_envs {
|
|
||||||
($env: expr, 2) => {{
|
|
||||||
use std::concat;
|
|
||||||
use std::env::var;
|
|
||||||
var(concat!($env, "_CLIENT_ID"))
|
|
||||||
.ok()
|
|
||||||
.zip(var(concat!($env, "_CLIENT_SECRET")).ok())
|
|
||||||
}};
|
|
||||||
($env: expr, 1) => {{
|
|
||||||
use std::concat;
|
|
||||||
use std::env::var;
|
|
||||||
var(concat!($env, "_CLIENT_ID")).ok()
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
#[derive(Debug, Deref)]
|
|
||||||
struct Gitlab(SigninFlow<IsahcClient>);
|
|
||||||
#[derive(Debug, Deref)]
|
|
||||||
struct Github(SigninFlow<IsahcClient>);
|
|
||||||
#[derive(Debug, Deref)]
|
|
||||||
struct Google(SigninFlow<IsahcClient>);
|
|
||||||
#[derive(Debug, Deref)]
|
|
||||||
struct Amazon(SigninFlow<IsahcClient>);
|
|
||||||
|
|
||||||
struct OAuthSecrets {
|
|
||||||
client_id: String,
|
|
||||||
client_secret: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OAuthSecrets {
|
|
||||||
pub fn new1(client_id: String) -> Self {
|
|
||||||
Self {
|
|
||||||
client_secret: None,
|
|
||||||
client_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new2(client_id: String, client_secret: String) -> Self {
|
|
||||||
Self {
|
|
||||||
client_secret: Some(client_secret),
|
|
||||||
client_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) {
|
|
||||||
let schema = env_var("JET_API_SCHEMA").unwrap();
|
|
||||||
let host = env_var("JET_API_HOST").unwrap();
|
|
||||||
let uri = format!("{schema}://{host}");
|
|
||||||
let client = http_api_isahc_client::IsahcClient::new().unwrap();
|
|
||||||
|
|
||||||
if let Some((client_id, client_secret)) = oauth_envs!("GITHUB", 2) {
|
|
||||||
config
|
|
||||||
.app_data(Data::new(Github(SigninFlow::new(
|
|
||||||
client.clone(),
|
|
||||||
GithubProviderWithWebApplication::new(
|
|
||||||
client_id.to_owned(),
|
|
||||||
client_secret.to_owned(),
|
|
||||||
format!("{uri}/auth/github/callback").parse().unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
vec![GithubScope::PublicRepo, GithubScope::UserEmail],
|
|
||||||
GithubExtensionsBuilder,
|
|
||||||
))))
|
|
||||||
.service(github_callback);
|
|
||||||
}
|
|
||||||
if let Some((client_id, client_secret)) = oauth_envs!("GITLAB", 2) {
|
|
||||||
config
|
|
||||||
.app_data(Data::new(Gitlab(SigninFlow::new(
|
|
||||||
client.clone(),
|
|
||||||
GitlabProviderForEndUsers::new(
|
|
||||||
BASE_URL_GITLAB_COM,
|
|
||||||
client_id.to_owned(),
|
|
||||||
client_secret.to_owned(),
|
|
||||||
oauth2_core::types::RedirectUri::Url(
|
|
||||||
format!("{uri}/auth/gitlab/callback").parse().unwrap(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
vec![
|
|
||||||
GitlabScope::Openid,
|
|
||||||
GitlabScope::Profile,
|
|
||||||
GitlabScope::Email,
|
|
||||||
GitlabScope::ReadUser,
|
|
||||||
],
|
|
||||||
GitlabExtensionsBuilder,
|
|
||||||
))))
|
|
||||||
.service(gitlab_callback);
|
|
||||||
}
|
|
||||||
if let Some((client_id, client_secret)) = oauth_envs!("GOOGLE", 2) {
|
|
||||||
config
|
|
||||||
.app_data(Data::new(Google(SigninFlow::new(
|
|
||||||
client.clone(),
|
|
||||||
GoogleProviderForWebServerApps::new(
|
|
||||||
client_id.clone(),
|
|
||||||
client_secret.clone(),
|
|
||||||
oauth2_core::types::RedirectUri::Url(
|
|
||||||
format!("{uri}/auth/google/callback").parse().unwrap(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.configure(|x| {
|
|
||||||
x.access_type = Some(GoogleProviderForWebServerAppsAccessType::Offline);
|
|
||||||
x.include_granted_scopes = Some(true);
|
|
||||||
}),
|
|
||||||
vec![
|
|
||||||
GoogleScope::Email,
|
|
||||||
GoogleScope::Profile,
|
|
||||||
GoogleScope::Openid,
|
|
||||||
],
|
|
||||||
GoogleExtensionsBuilder,
|
|
||||||
))))
|
|
||||||
.service(google_callback);
|
|
||||||
}
|
|
||||||
if let Some((client_id, client_secret)) = oauth_envs!("AMAZON", 2) {
|
|
||||||
config
|
|
||||||
.app_data(Data::new(Amazon(SigninFlow::new(
|
|
||||||
client.clone(),
|
|
||||||
AmazonProviderWithWebServices::new(
|
|
||||||
client_id.to_owned(),
|
|
||||||
client_secret.to_owned(),
|
|
||||||
oauth2_core::types::RedirectUri::Url(
|
|
||||||
format!("{uri}/auth/amazon/callback").parse().unwrap(),
|
|
||||||
),
|
|
||||||
AmazonTokenUrlRegion::NA,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
vec![AmazonScope::Profile, AmazonScope::PostalCode],
|
|
||||||
AmazonExtensionsBuilder,
|
|
||||||
))))
|
|
||||||
.service(amazon_callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.service(social_auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/social-auth")]
|
|
||||||
async fn social_auth() -> HttpResponse {
|
|
||||||
HttpResponse::NotImplemented().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/auth/gitlab/callback")]
|
|
||||||
async fn gitlab_callback(flow: Data<Gitlab>, query: Query<String>) -> HttpResponse {
|
|
||||||
let query = query.into_inner();
|
|
||||||
debug!("gitlab callback {query:?}");
|
|
||||||
|
|
||||||
let config = SigninFlowHandleCallbackByQueryConfiguration::new();
|
|
||||||
let ret = flow.handle_callback_by_query(query, config).await;
|
|
||||||
info!("gitlab {ret:?}");
|
|
||||||
|
|
||||||
HttpResponse::NotImplemented().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/auth/github/callback")]
|
|
||||||
async fn github_callback(flow: Data<Github>) -> HttpResponse {
|
|
||||||
HttpResponse::NotImplemented().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/auth/google/callback")]
|
|
||||||
async fn google_callback(flow: Data<Google>) -> HttpResponse {
|
|
||||||
HttpResponse::NotImplemented().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/auth/amazon/callback")]
|
|
||||||
async fn amazon_callback(flow: Data<Amazon>) -> HttpResponse {
|
|
||||||
HttpResponse::NotImplemented().finish()
|
|
||||||
}
|
|
@ -17,6 +17,8 @@ pub const APPLICATION_NAME: &str = "jet-api";
|
|||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
dotenv::from_filename(".env.development").ok();
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let addr = env::var("JET_API_ADDR").unwrap_or_else(|_| "0.0.0.0:7865".to_owned());
|
let addr = env::var("JET_API_ADDR").unwrap_or_else(|_| "0.0.0.0:7865".to_owned());
|
||||||
|
@ -16,7 +16,7 @@ impl JsonError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, derive_more::Display)]
|
#[derive(Debug, Clone, derive_more::Display)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[display(fmt = "Database connection error")]
|
#[display(fmt = "Database connection error")]
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
use std::ops::Add;
|
||||||
|
|
||||||
|
use actix_jwt_session::JwtTtl;
|
||||||
|
use entities::users::Model as User;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -11,9 +15,9 @@ pub enum Audience {
|
|||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct AppClaims {
|
pub struct AppClaims {
|
||||||
#[serde(rename = "exp")]
|
#[serde(rename = "exp")]
|
||||||
pub expiration_time: u64,
|
pub expiration_time: i64,
|
||||||
#[serde(rename = "iat")]
|
#[serde(rename = "iat")]
|
||||||
pub issues_at: usize,
|
pub issued_at: i64,
|
||||||
/// Account login
|
/// Account login
|
||||||
#[serde(rename = "sub")]
|
#[serde(rename = "sub")]
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
@ -24,7 +28,7 @@ pub struct AppClaims {
|
|||||||
#[serde(rename = "aci")]
|
#[serde(rename = "aci")]
|
||||||
pub account_id: Uuid,
|
pub account_id: Uuid,
|
||||||
#[serde(rename = "nbf")]
|
#[serde(rename = "nbf")]
|
||||||
pub not_before: u64,
|
pub not_before: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_jwt_session::Claims for AppClaims {
|
impl actix_jwt_session::Claims for AppClaims {
|
||||||
@ -41,4 +45,19 @@ impl AppClaims {
|
|||||||
pub fn account_id(&self) -> Uuid {
|
pub fn account_id(&self) -> Uuid {
|
||||||
self.account_id
|
self.account_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_user(user: User, ttl: JwtTtl) -> Self {
|
||||||
|
let ttl = ttl.0;
|
||||||
|
let ttl = chrono::Duration::seconds(ttl.whole_seconds());
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
Self {
|
||||||
|
expiration_time: now.naive_utc().add(ttl).timestamp(),
|
||||||
|
issued_at: now.timestamp(),
|
||||||
|
subject: user.email.clone().unwrap_or_default(),
|
||||||
|
audience: Audience::Web,
|
||||||
|
jwt_id: Uuid::new_v4(),
|
||||||
|
account_id: user.id,
|
||||||
|
not_before: now.timestamp(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,12 +56,15 @@ pub enum FileMsg {
|
|||||||
pub enum SignInMedium {
|
pub enum SignInMedium {
|
||||||
#[display(fmt = "MAGIC_LINK")]
|
#[display(fmt = "MAGIC_LINK")]
|
||||||
MagicLink,
|
MagicLink,
|
||||||
|
#[display(fmt = "oauth")]
|
||||||
|
OAuth,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignInMedium {
|
impl SignInMedium {
|
||||||
pub fn as_str(self) -> &'static str {
|
pub fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::MagicLink => "MAGIC_LINK",
|
Self::MagicLink => "MAGIC_LINK",
|
||||||
|
Self::OAuth => "oauth",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1937
plane_db.sql
1937
plane_db.sql
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user