diff --git a/Cargo.lock b/Cargo.lock index 84f9cde..848b4f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,9 +86,9 @@ dependencies = [ [[package]] name = "actix-jwt-session" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6317d3303618eea36d68898bf720ce94d8b5bb2bf1a23c8ffdf714024f0a49a6" +checksum = "5417207a8cf4ac0c420464ea0b457143a4a172a0ee39eb5adf6cb70fa21c3065" dependencies = [ "actix-web", "argon2", @@ -2047,9 +2047,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libnghttp2-sys" -version = "0.1.8+1.55.1" +version = "0.1.9+1.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340" +checksum = "b57e858af2798e167e709b9d969325b6d8e9d50232fcbc494d7d54f976854a64" dependencies = [ "cc", "libc", @@ -2079,9 +2079,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.14" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295c17e837573c8c821dbaeb3cceb3d745ad082f7572191409e69cbc1b3fd050" +checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" dependencies = [ "cc", "libc", @@ -2714,18 +2714,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", @@ -3680,9 +3680,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] @@ -3741,9 +3741,9 @@ checksum = "794e44574226fc701e3be5c651feb7939038fc67fb73f6f4dd5c4ba90fd3be70" [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -3752,9 +3752,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" dependencies = [ "itoa", "ryu", @@ -5144,9 +5144,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.34" +version = "0.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d" dependencies = [ "memchr", ] diff --git a/crates/jet-api/src/http/api/authentication.rs b/crates/jet-api/src/http/api/authentication.rs index 1c5ab3b..80df359 100644 --- a/crates/jet-api/src/http/api/authentication.rs +++ b/crates/jet-api/src/http/api/authentication.rs @@ -4,12 +4,11 @@ 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, HttpRequest, HttpResponse}; +use actix_web::{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; @@ -28,9 +27,9 @@ pub struct AuthResponseBody { pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) { config .service(email_check::email_check) - .service(oauth) .service(sign_in::sign_in) - .service(sign_up::sign_up); + .service(sign_up::sign_up) + .service(sign_out::sign_out); social_auth::configure(http_client, config); } diff --git a/crates/jet-api/src/http/api/authentication/sign_out.rs b/crates/jet-api/src/http/api/authentication/sign_out.rs index 5aaec22..b9e7437 100644 --- a/crates/jet-api/src/http/api/authentication/sign_out.rs +++ b/crates/jet-api/src/http/api/authentication/sign_out.rs @@ -1,14 +1,52 @@ -use actix_jwt_session::{Authenticated, SessionStorage}; +use actix_jwt_session::{Authenticated, RefreshToken, SessionStorage}; +use actix_web::HttpRequest; use actix_web::{post, web::Data, HttpResponse}; -use sea_orm::DatabaseConnection; +use entities::prelude::Users; +use entities::users::{ActiveModel as UserModel, Column}; +use sea_orm::prelude::*; +use sea_orm::Set; +use serde_json::json; +use crate::models::{Error, JsonError}; use crate::session::AppClaims; +use crate::utils::extract_req_ip; #[post("/sign-out")] pub async fn sign_out( - _db: Data, - claims: Authenticated, + req: HttpRequest, + db: Data, + claims: Authenticated, session: Data, -) -> HttpResponse { - HttpResponse::NotImplemented().finish() +) -> Result { + let Ok(access_token) = session.find_jwt::(claims.access_jti()).await else { + return Err(JsonError::new("No refresh token provided")); + }; + let mut user: UserModel = match Users::find() + .filter(Column::Id.eq(access_token.account_id())) + .one(&**db) + .await + { + Ok(Some(user)) => user, + Ok(None) => { + return Err(JsonError::new( + "User for this token does not exists. Please contact administrator", + )) + } + Err(e) => { + tracing::error!("Failed to connect to database on sign-out: {e}"); + return Err(JsonError::new(Error::DatabaseError)); + } + } + .into(); + + user.last_logout_ip = Set(extract_req_ip(&req)?); + user.last_logout_time = Set(Some(chrono::Utc::now().fixed_offset())); + user.save(&**db).await.map_err(|e| { + tracing::error!("Failed to update user in sign-out: {e}"); + return JsonError::new(Error::DatabaseError); + })?; + + session.erase::(access_token.jwt_id).await.ok(); + + Ok(HttpResponse::Ok().json(json!({ "message": "success" } ))) } diff --git a/crates/jet-api/src/main.rs b/crates/jet-api/src/main.rs index fd61c8e..9936409 100644 --- a/crates/jet-api/src/main.rs +++ b/crates/jet-api/src/main.rs @@ -22,7 +22,7 @@ async fn main() { dotenv::from_filename(".env.development").ok(); tracing_subscriber::fmt::init(); - if let Ok(entry_dsn) = env::var("SENTRY_DSN") { + if let Ok(sentry_dsn) = env::var("SENTRY_DSN") { let _guard = sentry::init(sentry_dsn); } @@ -54,6 +54,8 @@ async fn main() { .with_refresh_header(REFRESH_HEADER_NAME) // Check if cookie JWT exists and contains encoded JWT .with_refresh_cookie(REFRESH_COOKIE_NAME) + .with_jwt_json(&["refresh_token"]) + .with_jwt_json(&["access_token"]) .finish(); let jwt_ttl = JwtTtl(Duration::days(9999)); let refresh_ttl = RefreshTtl(Duration::days(3 * 31 * 999)); diff --git a/crates/jet-api/src/utils/mod.rs b/crates/jet-api/src/utils/mod.rs index 7916921..0b056e4 100644 --- a/crates/jet-api/src/utils/mod.rs +++ b/crates/jet-api/src/utils/mod.rs @@ -23,17 +23,34 @@ use uuid::Uuid; use crate::http::OAuthError; 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 +pub fn extract_req_ip(req: &HttpRequest) -> Result { + Ok(req + .peer_addr() + .ok_or(AuthError::NoPeerAddr)? + .ip() + .to_string()) +} + +pub fn extract_req_uagent(req: &HttpRequest) -> Result { + Ok(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)) + .to_string()) +} + +pub fn extract_req_current_site(req: &HttpRequest) -> Result { + Ok(req.uri().host().ok_or(Error::NoHost)?.to_owned()) +} + +pub fn extract_req_info(req: &HttpRequest) -> Result<(String, String, String), Error> { + Ok(( + extract_req_ip(&*req)?, + extract_req_uagent(&*req)?, + extract_req_current_site(req)?, + )) } pub async fn invites_to_membership(