From 11092a41a5a99e50ed4ce2d2548778d6840f21af Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Tue, 20 Dec 2022 20:29:25 +0100 Subject: [PATCH 01/10] feat: prolong session duration to 14 days --- core/src/database/mutation/session.rs | 17 +++++- core/src/services/session_service.rs | 59 +++++++++++-------- entity/src/session.rs | 1 + .../src/m20221025_154422_create_session.rs | 2 + 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/core/src/database/mutation/session.rs b/core/src/database/mutation/session.rs index c540398..0806996 100644 --- a/core/src/database/mutation/session.rs +++ b/core/src/database/mutation/session.rs @@ -1,4 +1,4 @@ -use chrono::{Utc, Duration}; +use chrono::{Utc, Duration, NaiveDateTime}; use ::entity::session; use sea_orm::{*, prelude::Uuid}; @@ -21,13 +21,26 @@ impl Mutation { created_at: Set(Utc::now().naive_local()), expires_at: Set(Utc::now() .naive_local() - .checked_add_signed(Duration::days(1)) + .checked_add_signed(Duration::days(14)) .unwrap()), + updated_at: Set(Utc::now().naive_local()) } .insert(db) .await } + pub async fn update_session_expiration(db: &DbConn, + session: session::Model, + expires_at: NaiveDateTime, + ) -> Result { + let mut session = session.into_active_model(); + + session.expires_at = Set(expires_at); + session.updated_at = Set(Utc::now().naive_local()); + + session.update(db).await + } + pub async fn delete_session(db: &DbConn, session_id: Uuid) -> Result { session::ActiveModel { id: Set(session_id), diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index 78bc054..f275913 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -1,7 +1,8 @@ use std::cmp::min; -use entity::{admin, candidate}; -use sea_orm::{prelude::Uuid, DatabaseConnection, ModelTrait, DbConn}; +use chrono::Duration; +use entity::{admin, candidate, session}; +use sea_orm::{prelude::Uuid, ModelTrait, DbConn}; use crate::{ crypto::{self}, @@ -19,7 +20,7 @@ pub(in crate::services) struct SessionService; impl SessionService { /// Delete n old sessions for user async fn delete_old_sessions( - db: &DatabaseConnection, + db: &DbConn, user_id: Option, admin_id: Option, keep_n_recent: usize, @@ -42,7 +43,7 @@ impl SessionService { /// Authenticate user by application id and password and generate a new session pub async fn new_session( - db: &DatabaseConnection, + db: &DbConn, user_id: Option, admin_id: Option, password: String, @@ -97,14 +98,7 @@ impl SessionService { // user is authenticated, generate a new session let random_uuid: Uuid = Uuid::new_v4(); - let session = - match Mutation::insert_session(db, user_id, admin_id, random_uuid, ip_addr).await { - Ok(session) => session, - Err(e) => { - eprintln!("Error creating session: {}", e); - return Err(ServiceError::DbError(e)); - } - }; + let session = Mutation::insert_session(db, user_id, admin_id, random_uuid, ip_addr).await?; // delete old sessions SessionService::delete_old_sessions(db, user_id, admin_id, 3) @@ -118,28 +112,43 @@ impl SessionService { Self::delete_old_sessions(db, user_id, admin_id, 0).await } + /// Check if session is valid + async fn is_valid(db: &DbConn, session: &session::Model) -> Result { + let now = chrono::Utc::now().naive_utc(); + if now >= session.expires_at { + Mutation::delete_session(db, session.id).await?; + Ok(false) + } else { + Ok(true) + } + + } + + /// If 1 day or more since last update, extend session duration to 14 days + async fn extend_session_duration_to_14_days(db: &DbConn, session: session::Model) -> Result<(), ServiceError> { + let now = chrono::Utc::now().naive_utc(); + if now >= session.updated_at.checked_add_signed(Duration::days(1)).ok_or(ServiceError::Unauthorized)? { + let new_expires_at = now.checked_add_signed(Duration::days(14)).ok_or(ServiceError::Unauthorized)?; + Mutation::update_session_expiration(db, session, new_expires_at).await?; + } + Ok(()) + } + /// Authenticate user by session id /// Return user model if session is valid pub async fn auth_user_session( - db: &DatabaseConnection, + db: &DbConn, uuid: Uuid, ) -> Result { - let session = match Query::find_session_by_uuid(db, uuid).await { - Ok(session) => match session { - Some(session) => session, - None => return Err(ServiceError::UserNotFoundBySessionId), - }, - Err(e) => return Err(ServiceError::DbError(e)), - }; + let session = Query::find_session_by_uuid(db, uuid).await? + .ok_or(ServiceError::UserNotFoundBySessionId)?; - let now = chrono::Utc::now().naive_utc(); - // check if session is expired - if now > session.expires_at { - // delete session - Mutation::delete_session(db, session.id).await.unwrap(); + if !Self::is_valid(db, &session).await? { return Err(ServiceError::ExpiredSession); } + Self::extend_session_duration_to_14_days(db, session.clone()).await?; + let candidate = session.find_related(candidate::Entity).one(db).await; let admin = session.find_related(admin::Entity).one(db).await; diff --git a/entity/src/session.rs b/entity/src/session.rs index e0b5829..2d0599b 100644 --- a/entity/src/session.rs +++ b/entity/src/session.rs @@ -12,6 +12,7 @@ pub struct Model { pub ip_address: String, pub created_at: DateTime, pub expires_at: DateTime, + pub updated_at: DateTime, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/migration/src/m20221025_154422_create_session.rs b/migration/src/m20221025_154422_create_session.rs index 3bb8b8c..c3e06e3 100644 --- a/migration/src/m20221025_154422_create_session.rs +++ b/migration/src/m20221025_154422_create_session.rs @@ -23,6 +23,7 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Session::IpAddress).string().not_null()) .col(ColumnDef::new(Session::CreatedAt).date_time().not_null()) .col(ColumnDef::new(Session::ExpiresAt).date_time().not_null()) + .col(ColumnDef::new(Session::UpdatedAt).date_time().not_null()) .to_owned(), ) .await @@ -44,4 +45,5 @@ pub enum Session { IpAddress, CreatedAt, ExpiresAt, + UpdatedAt, } From 2f12211aa7d34fb5d3103a620c3f5650d382a2ba Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Tue, 20 Dec 2022 20:30:59 +0100 Subject: [PATCH 02/10] style: reformat based on sea-orm-cli geneartor --- entity/src/candidate.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index fdfbc46..8a1e76e 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -30,16 +30,10 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { - #[sea_orm(has_many = "super::session::Entity")] - Session, #[sea_orm(has_many = "super::parent::Entity")] Parent, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Session.def() - } + #[sea_orm(has_many = "super::session::Entity")] + Session, } impl Related for Entity { @@ -48,4 +42,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Session.def() + } +} + impl ActiveModelBehavior for ActiveModel {} From 4cf408ab2f5c3f360ba8f4ed37bb375ef1b4de84 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Wed, 21 Dec 2022 16:08:47 +0100 Subject: [PATCH 03/10] feat: auth trait --- api/src/guards/request/auth/admin.rs | 1 + api/src/guards/request/auth/candidate.rs | 1 + api/src/routes/admin.rs | 2 +- api/src/routes/candidate.rs | 2 +- core/Cargo.toml | 3 +- core/src/models/auth.rs | 14 +++++ core/src/models/mod.rs | 3 +- core/src/services/admin_service.rs | 30 ++++++---- core/src/services/candidate_service.rs | 73 +++++++++++++----------- core/src/services/session_service.rs | 53 +++++------------ 10 files changed, 95 insertions(+), 87 deletions(-) create mode 100644 core/src/models/auth.rs diff --git a/api/src/guards/request/auth/admin.rs b/api/src/guards/request/auth/admin.rs index 984c082..4871bf5 100644 --- a/api/src/guards/request/auth/admin.rs +++ b/api/src/guards/request/auth/admin.rs @@ -1,5 +1,6 @@ use entity::admin::Model as Admin; use log::info; +use portfolio_core::models::auth::AuthenticableTrait; use portfolio_core::sea_orm::prelude::Uuid; use portfolio_core::services::admin_service::AdminService; use rocket::http::Status; diff --git a/api/src/guards/request/auth/candidate.rs b/api/src/guards/request/auth/candidate.rs index 08d5fe3..0cd3d48 100644 --- a/api/src/guards/request/auth/candidate.rs +++ b/api/src/guards/request/auth/candidate.rs @@ -1,4 +1,5 @@ use entity::candidate::Model as Candidate; +use portfolio_core::models::auth::AuthenticableTrait; use portfolio_core::sea_orm::prelude::Uuid; use portfolio_core::services::candidate_service::CandidateService; use rocket::http::Status; diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 02c0112..7c8e0f2 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -2,7 +2,7 @@ use std::net::{SocketAddr, IpAddr, Ipv4Addr}; use portfolio_core::{ crypto::random_12_char_string, - services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::candidate::{BaseCandidateResponse, CreateCandidateResponse, ApplicationDetails}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv, + services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse, ApplicationDetails}, auth::AuthenticableTrait}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv, }; use requests::{AdminLoginRequest, RegisterRequest}; use rocket::http::{Cookie, Status, CookieJar}; diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index 91e0cab..a50df9b 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -1,5 +1,6 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use portfolio_core::models::auth::AuthenticableTrait; use portfolio_core::models::candidate::ApplicationDetails; use portfolio_core::sea_orm::prelude::Uuid; use portfolio_core::services::application_service::ApplicationService; @@ -227,7 +228,6 @@ pub async fn submit_portfolio( #[post("/delete")] pub async fn delete_portfolio( - conn: Connection<'_, Db>, session: CandidateAuth, ) -> Result<(), Custom> { let candidate: entity::candidate::Model = session.into(); diff --git a/core/Cargo.toml b/core/Cargo.toml index be3962a..dc63198 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,8 +15,9 @@ serde = { version = "^1.0", features = ["derive"] } # csv csv = "^1.1" -# error +async-trait = "0.1.60" +# error thiserror = "^1.0" # env diff --git a/core/src/models/auth.rs b/core/src/models/auth.rs new file mode 100644 index 0000000..4c9e1d6 --- /dev/null +++ b/core/src/models/auth.rs @@ -0,0 +1,14 @@ +use async_trait::async_trait; +use sea_orm::{prelude::Uuid, DbConn}; + +use crate::error::ServiceError; + + +#[async_trait] +pub trait AuthenticableTrait { + type User; + // fn password_valid(user: T); + async fn login(db: &DbConn, user: i32, password: String, ip_addr: String) -> Result<(String, String), ServiceError>; + async fn auth(db: &DbConn, session_id: Uuid) -> Result; + async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError>; +} \ No newline at end of file diff --git a/core/src/models/mod.rs b/core/src/models/mod.rs index 6cd26ab..4c3b0be 100644 --- a/core/src/models/mod.rs +++ b/core/src/models/mod.rs @@ -1,2 +1,3 @@ pub mod candidate_details; -pub mod candidate; \ No newline at end of file +pub mod candidate; +pub mod auth; \ No newline at end of file diff --git a/core/src/services/admin_service.rs b/core/src/services/admin_service.rs index f393102..c4114d1 100644 --- a/core/src/services/admin_service.rs +++ b/core/src/services/admin_service.rs @@ -1,7 +1,8 @@ +use async_trait::async_trait; use entity::admin; use sea_orm::{prelude::Uuid, DbConn}; -use crate::{crypto, error::ServiceError, Query, Mutation}; +use crate::{crypto, error::ServiceError, Query, Mutation, models::auth::AuthenticableTrait}; use super::session_service::{AdminUser, SessionService}; @@ -19,13 +20,20 @@ impl AdminService { Ok(private_key) } +} - pub async fn login( +#[async_trait] +impl AuthenticableTrait for AdminService { + type User = admin::Model; + + async fn login( db: &DbConn, admin_id: i32, password: String, ip_addr: String, ) -> Result<(String, String), ServiceError> { + let admin = Query::find_admin_by_id(db, admin_id).await?.ok_or(ServiceError::InvalidCredentials)?; + let session_id = SessionService::new_session(db, None, Some(admin_id), @@ -34,21 +42,21 @@ impl AdminService { ) .await?; - let private_key = Self::decrypt_private_key(db, admin_id, password).await?; + let private_key = Self::decrypt_private_key(db, admin.id, password).await?; Ok((session_id, private_key)) } - pub async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> { - Mutation::delete_session(db, session_id).await?; - Ok(()) - } - - pub async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { + async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { match SessionService::auth_user_session(db, session_uuid).await? { AdminUser::Admin(admin) => Ok(admin), AdminUser::Candidate(_) => Err(ServiceError::Unauthorized), } } + + async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> { + Mutation::delete_session(db, session_id).await?; + Ok(()) + } } #[cfg(test)] @@ -65,7 +73,7 @@ mod admin_tests { #[tokio::test] async fn test_admin_login() -> Result<(), ServiceError> { let db = get_memory_sqlite_connection().await; - let _ = admin::ActiveModel { + let admin = admin::ActiveModel { id: Set(1), name: Set("Admin".to_owned()), public_key: Set("age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_owned()), @@ -80,7 +88,7 @@ mod admin_tests { .insert(&db) .await?; - let (session_id, _private_key) = AdminService::login(&db, 1, "test".to_owned(), "127.0.0.1".to_owned()).await?; + let (session_id, _private_key) = AdminService::login(&db, admin.id, "test".to_owned(), "127.0.0.1".to_owned()).await?; let logged_admin = AdminService::auth(&db, session_id.parse().unwrap()).await?; diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index dc0639a..6d7a481 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use entity::candidate; use sea_orm::{prelude::Uuid, DbConn}; @@ -5,7 +6,7 @@ use crate::{ models::{candidate_details::{EncryptedApplicationDetails, EncryptedString, EncryptedCandidateDetails}, candidate::CandidateDetails}, crypto::{self, hash_password}, error::ServiceError, - Mutation, Query, models::candidate::{BaseCandidateResponse, CreateCandidateResponse}, utils::db::get_recipients, + Mutation, Query, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse}, auth::AuthenticableTrait}, utils::db::get_recipients, }; use super::{session_service::{AdminUser, SessionService}, application_service::ApplicationService, portfolio_service::PortfolioService}; @@ -140,11 +141,6 @@ impl CandidateService { ) } - pub async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> { - Mutation::delete_session(db, session_id).await?; - Ok(()) - } - pub async fn delete_candidate(db: &DbConn, candidate: candidate::Model) -> Result<(), ServiceError> { PortfolioService::delete_candidate_root(candidate.application).await?; @@ -218,34 +214,6 @@ impl CandidateService { Ok(private_key) } - pub async fn login( - db: &DbConn, - candidate_id: i32, - password: String, - ip_addr: String, - ) -> Result<(String, String), ServiceError> { - let candidate = Query::find_candidate_by_id(db, candidate_id) - .await? - .ok_or(ServiceError::CandidateNotFound)?; - - let session_id = - SessionService::new_session(db, Some(candidate_id), None, password.clone(), ip_addr) - .await?; - - let private_key = Self::decrypt_private_key(candidate, password).await?; - Ok((session_id, private_key)) - } - - pub async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { - match SessionService::auth_user_session(db, session_uuid).await { - Ok(user) => match user { - AdminUser::Candidate(candidate) => Ok(candidate), - AdminUser::Admin(_) => Err(ServiceError::Unauthorized), - }, - Err(e) => Err(e), - } - } - fn is_application_id_valid(application_id: i32) -> bool { let s = &application_id.to_string(); if s.len() <= 3 { @@ -257,10 +225,47 @@ impl CandidateService { } } +#[async_trait] +impl AuthenticableTrait for CandidateService { + type User = candidate::Model; + + async fn login( + db: &DbConn, + application_id: i32, + password: String, + ip_addr: String, + ) -> Result<(String, String), ServiceError> { + let candidate = Query::find_candidate_by_id(db, application_id) + .await? + .ok_or(ServiceError::CandidateNotFound)?; + + let session_id = SessionService::new_session(db, Some(application_id), None, password.clone(), ip_addr) + .await?; + + let private_key = Self::decrypt_private_key(candidate, password).await?; + Ok((session_id, private_key)) + } + + async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { + match SessionService::auth_user_session(db, session_uuid).await { + Ok(user) => match user { + AdminUser::Candidate(candidate) => Ok(candidate), + AdminUser::Admin(_) => Err(ServiceError::Unauthorized), + }, + Err(e) => Err(e), + } + } + async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> { + Mutation::delete_session(db, session_id).await?; + Ok(()) + } +} + #[cfg(test)] pub mod tests { use sea_orm::{DbConn}; + use crate::models::auth::AuthenticableTrait; use crate::models::candidate_details::tests::assert_all_application_details; use crate::utils::db::get_memory_sqlite_connection; use crate::{crypto, services::candidate_service::CandidateService, Mutation}; diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index f275913..0f3bb97 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -44,64 +44,41 @@ impl SessionService { /// Authenticate user by application id and password and generate a new session pub async fn new_session( db: &DbConn, - user_id: Option, + candidate_id: Option, admin_id: Option, password: String, ip_addr: String, ) -> Result { - if user_id.is_none() && admin_id.is_none() { + if candidate_id.is_none() && admin_id.is_none() { return Err(ServiceError::UserNotFoundBySessionId); } - if admin_id.is_none() { - // unwrap is safe here - let candidate = match Query::find_candidate_by_id(db, user_id.unwrap()).await { - Ok(candidate) => match candidate { - Some(candidate) => candidate, - None => return Err(ServiceError::CandidateNotFound), - }, - Err(e) => return Err(ServiceError::DbError(e)), - }; + if let Some(candidate_id) = candidate_id { + let candidate = Query::find_candidate_by_id(db, candidate_id).await? + .ok_or(ServiceError::CandidateNotFound)?; // compare passwords - match crypto::verify_password(password.clone(), candidate.code.clone()).await { - Ok(valid) => { - if !valid { - return Err(ServiceError::InvalidCredentials); - } - } - Err(_) => return Err(ServiceError::InvalidCredentials), + if !crypto::verify_password(password.clone(), candidate.code.clone()).await? { + return Err(ServiceError::InvalidCredentials); } } - if user_id.is_none() { - // unwrap is safe here - let admin = match Query::find_admin_by_id(db, admin_id.unwrap()).await { - Ok(admin) => match admin { - Some(admin) => admin, - None => return Err(ServiceError::CandidateNotFound), - }, - Err(e) => return Err(ServiceError::DbError(e)), - }; + if let Some(admin_id) = admin_id { + let admin = Query::find_admin_by_id(db, admin_id).await? + .ok_or(ServiceError::InvalidCredentials)?; // compare passwords - match crypto::verify_password(password.clone(), admin.password.clone()).await { - Ok(valid) => { - if !valid { - return Err(ServiceError::InvalidCredentials); - } - } - Err(_) => return Err(ServiceError::InvalidCredentials), + if !crypto::verify_password(password.clone(), admin.password.clone()).await? { + return Err(ServiceError::InvalidCredentials); } } - // user is authenticated, generate a new session + let random_uuid: Uuid = Uuid::new_v4(); - let session = Mutation::insert_session(db, user_id, admin_id, random_uuid, ip_addr).await?; + let session = Mutation::insert_session(db, candidate_id, admin_id, random_uuid, ip_addr).await?; - // delete old sessions - SessionService::delete_old_sessions(db, user_id, admin_id, 3) + SessionService::delete_old_sessions(db, candidate_id, admin_id, 3) .await .ok(); From 0b1c93c336dd28b98356ea6b50337dd0c1adb4a7 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Wed, 21 Dec 2022 16:13:45 +0100 Subject: [PATCH 04/10] fix: updated_at in tests --- core/src/database/query/session.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs index 854a2d9..19e1b30 100644 --- a/core/src/database/query/session.rs +++ b/core/src/database/query/session.rs @@ -48,6 +48,7 @@ mod tests { ip_address: Set("10.10.10.10".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), expires_at: Set(chrono::offset::Local::now().naive_local()), + updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() } .insert(&db) @@ -85,6 +86,7 @@ mod tests { ip_address: Set("10.10.10.10".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), expires_at: Set(chrono::offset::Local::now().naive_local()), + updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() } .insert(&db) @@ -114,6 +116,7 @@ mod tests { ip_address: Set("10.10.10.10".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), expires_at: Set(chrono::offset::Local::now().naive_local()), + updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() } .insert(&db) From 996b45d62ea26addc9c1c737328ed0e4a7521ad0 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Wed, 21 Dec 2022 18:27:58 +0100 Subject: [PATCH 05/10] feat!: admin session table --- api/src/routes/admin.rs | 3 +- api/src/routes/candidate.rs | 5 +- core/src/database/mutation/admin_session.rs | 49 ++++++ core/src/database/mutation/mod.rs | 3 +- core/src/database/mutation/session.rs | 31 ++-- core/src/database/query/session.rs | 37 +++-- core/src/models/auth.rs | 7 +- core/src/services/admin_service.rs | 70 ++++++-- core/src/services/candidate_service.rs | 87 ++++++++-- core/src/services/session_service.rs | 154 +++--------------- entity/Cargo.toml | 1 + entity/src/admin.rs | 8 +- entity/src/admin_session.rs | 47 ++++++ entity/src/candidate.rs | 16 +- entity/src/lib.rs | 2 + entity/src/mod.rs | 1 + entity/src/prelude.rs | 1 + entity/src/session.rs | 31 ++-- entity/src/session_trait.rs | 8 + migration/src/lib.rs | 5 +- .../src/m20221025_154422_create_session.rs | 6 +- ...m20221027_194728_session_create_user_fk.rs | 2 +- ...20221028_194728_session_create_admin_fk.rs | 6 +- .../m20221221_162232_create_admin_session.rs | 48 ++++++ 24 files changed, 399 insertions(+), 229 deletions(-) create mode 100644 core/src/database/mutation/admin_session.rs create mode 100644 entity/src/admin_session.rs create mode 100644 entity/src/session_trait.rs create mode 100644 migration/src/m20221221_162232_create_admin_session.rs diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 7c8e0f2..4224fa1 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -58,8 +58,9 @@ pub async fn logout(conn: Connection<'_, Db>, _session: AdminAuth, cookies: &Coo .ok_or(Custom(Status::Unauthorized, "No session cookie".to_string()))?; let session_id = Uuid::try_parse(cookie.value()) // unwrap would be safe here because of the auth guard .map_err(|e| Custom(Status::BadRequest, e.to_string()))?; + let session = Query::find_admin_session_by_uuid(db, session_id).await.unwrap().unwrap(); - let _res = AdminService::logout(db, session_id) + let _res = AdminService::logout(db, session) .await .map_err(to_custom_error)?; diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index a50df9b..39cea7f 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -1,5 +1,6 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use portfolio_core::Query; use portfolio_core::models::auth::AuthenticableTrait; use portfolio_core::models::candidate::ApplicationDetails; use portfolio_core::sea_orm::prelude::Uuid; @@ -59,8 +60,8 @@ pub async fn logout( ))?; let session_id = Uuid::try_parse(cookie.value()) // unwrap would be safe here because of the auth guard .map_err(|e| Custom(Status::BadRequest, e.to_string()))?; - - CandidateService::logout(db, session_id) + let session = Query::find_session_by_uuid(db, session_id).await.unwrap().unwrap(); // TODO + CandidateService::logout(db, session) .await .map_err(to_custom_error)?; diff --git a/core/src/database/mutation/admin_session.rs b/core/src/database/mutation/admin_session.rs new file mode 100644 index 0000000..e182381 --- /dev/null +++ b/core/src/database/mutation/admin_session.rs @@ -0,0 +1,49 @@ +use chrono::{Utc, Duration}; +use entity::{admin_session}; +use sea_orm::{DbConn, prelude::Uuid, DbErr, Set, ActiveModelTrait}; + +use crate::Mutation; + +impl Mutation { + pub async fn insert_admin_session( + db: &DbConn, + admin_id: i32, + random_uuid: Uuid, + ip_addr: String, + ) -> Result { + admin_session::ActiveModel { + id: Set(random_uuid), + admin_id: Set(Some(admin_id)), + ip_address: Set(ip_addr), + created_at: Set(Utc::now().naive_local()), + expires_at: Set(Utc::now() + .naive_local() + .checked_add_signed(Duration::days(1)) + .unwrap()), + updated_at: Set(Utc::now().naive_local()) + } + .insert(db) + .await + } + + /* pub async fn update_session_expiration(db: &DbConn, + session: session::Model, + expires_at: NaiveDateTime, + ) -> Result { + let mut session = session.into_active_model(); + + session.expires_at = Set(expires_at); + session.updated_at = Set(Utc::now().naive_local()); + + session.update(db).await + } + + pub async fn delete_admin_session(db: &DbConn, session: ad) -> Result { + session::ActiveModel { + id: Set(session_id), + ..Default::default() + } + .delete(db) + .await + } */ +} \ No newline at end of file diff --git a/core/src/database/mutation/mod.rs b/core/src/database/mutation/mod.rs index 1a654a9..f5afed2 100644 --- a/core/src/database/mutation/mod.rs +++ b/core/src/database/mutation/mod.rs @@ -2,4 +2,5 @@ pub(crate) struct Mutation; pub mod session; pub mod candidate; -pub mod parent; \ No newline at end of file +pub mod parent; +pub mod admin_session; \ No newline at end of file diff --git a/core/src/database/mutation/session.rs b/core/src/database/mutation/session.rs index 0806996..6b476de 100644 --- a/core/src/database/mutation/session.rs +++ b/core/src/database/mutation/session.rs @@ -1,22 +1,20 @@ use chrono::{Utc, Duration, NaiveDateTime}; -use ::entity::session; +use ::entity::{session, candidate}; use sea_orm::{*, prelude::Uuid}; use crate::Mutation; impl Mutation { - pub async fn insert_session( + pub async fn insert_candidate_session( db: &DbConn, - user_id: Option, - admin_id: Option, random_uuid: Uuid, + candidate_id: i32, ip_addr: String, ) -> Result { session::ActiveModel { id: Set(random_uuid), - user_id: Set(user_id), - admin_id: Set(admin_id), + candidate_id: Set(Some(candidate_id)), ip_address: Set(ip_addr), created_at: Set(Utc::now().naive_local()), expires_at: Set(Utc::now() @@ -41,19 +39,18 @@ impl Mutation { session.update(db).await } - pub async fn delete_session(db: &DbConn, session_id: Uuid) -> Result { - session::ActiveModel { - id: Set(session_id), - ..Default::default() - } - .delete(db) - .await + pub async fn delete_session(db: &DbConn, session: T) -> Result + where T: ActiveModelTrait + std::marker::Send + ActiveModelBehavior + { + session + .delete(db) + .await } } #[cfg(test)] mod tests { - use sea_orm::prelude::Uuid; + /* use sea_orm::prelude::Uuid; use crate::{utils::db::get_memory_sqlite_connection, Mutation, services::candidate_service::tests::put_user_data}; @@ -62,14 +59,14 @@ mod tests { let db = get_memory_sqlite_connection().await; let session_id = Uuid::new_v4(); - let (user, _) = put_user_data(&db).await; + let (candidate, _) = put_user_data(&db).await; - let session = Mutation::insert_session(&db, Some(user.application), None, session_id, "127.0.0.1".to_string()).await.unwrap(); + let session = Mutation::insert_candidate_session(&db, session_id, candidate.application, "127.0.0.1".to_string()).await.unwrap(); assert_eq!(session.id, session_id); let delete_result = Mutation::delete_session(&db, session_id).await.unwrap(); assert_eq!(delete_result.rows_affected, 1); - } + } */ } \ No newline at end of file diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs index 19e1b30..ba2eceb 100644 --- a/core/src/database/query/session.rs +++ b/core/src/database/query/session.rs @@ -1,5 +1,7 @@ use crate::Query; +use ::entity::prelude::AdminSession; +use ::entity::{candidate, admin, admin_session}; use ::entity::{session, session::Entity as Session}; use sea_orm::prelude::Uuid; use sea_orm::*; @@ -12,8 +14,23 @@ impl Query { Session::find_by_id(uuid).one(db).await } + pub async fn find_admin_session_by_uuid( + db: &DbConn, + uuid: Uuid, + ) -> Result, DbErr> { + AdminSession::find_by_id(uuid).one(db).await + } + + pub async fn find_related_candidate_sessions(db: &DbConn, candidate: candidate::Model) -> Result, DbErr> { + candidate.find_related(Session).all(db).await + } + + pub async fn find_related_admin_sessions(db: &DbConn, admin: admin::Model) -> Result, DbErr> { + admin.find_related(admin_session::Entity).all(db).await + } + // find session by user id - pub async fn find_sessions_by_user_id( + /* pub async fn find_sessions_by_user_id( db: &DbConn, user_id: Option, admin_id: Option, @@ -27,12 +44,12 @@ impl Query { } .all(db) .await - } + } */ } #[cfg(test)] mod tests { - use entity::{session, admin, candidate}; + use entity::{session, admin, candidate, admin_session}; use sea_orm::ActiveValue::NotSet; use sea_orm::{prelude::Uuid, ActiveModelTrait, Set}; @@ -65,7 +82,7 @@ mod tests { const APPLICATION_ID: i32 = 103158; - candidate::ActiveModel { + let candidate = candidate::ActiveModel { application: Set(APPLICATION_ID), code: Set("test".to_string()), public_key: Set("test".to_string()), @@ -81,8 +98,7 @@ mod tests { session::ActiveModel { id: Set(Uuid::new_v4()), - user_id: Set(Some(APPLICATION_ID)), - admin_id: NotSet, + candidate_id: Set(Some(APPLICATION_ID)), ip_address: Set("10.10.10.10".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), expires_at: Set(chrono::offset::Local::now().naive_local()), @@ -95,7 +111,7 @@ mod tests { const ADMIN_ID: i32 = 1; - admin::ActiveModel { + let admin = admin::ActiveModel { id: Set(ADMIN_ID), name: Set("admin".to_string()), public_key: Set("test".to_string()), @@ -109,9 +125,8 @@ mod tests { .await .unwrap(); - session::ActiveModel { + admin_session::ActiveModel { id: Set(Uuid::new_v4()), - user_id: NotSet, admin_id: Set(Some(ADMIN_ID)), ip_address: Set("10.10.10.10".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), @@ -123,10 +138,10 @@ mod tests { .await .unwrap(); - let sessions = Query::find_sessions_by_user_id(&db, Some(APPLICATION_ID), None).await.unwrap(); + let sessions = Query::find_related_candidate_sessions(&db, candidate).await.unwrap(); assert_eq!(sessions.len(), 1); - let sessions = Query::find_sessions_by_user_id(&db, None, Some(ADMIN_ID)).await.unwrap(); + let sessions = Query::find_related_admin_sessions(&db, admin).await.unwrap(); assert_eq!(sessions.len(), 1); } } diff --git a/core/src/models/auth.rs b/core/src/models/auth.rs index 4c9e1d6..45bc5eb 100644 --- a/core/src/models/auth.rs +++ b/core/src/models/auth.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use entity::session; use sea_orm::{prelude::Uuid, DbConn}; use crate::error::ServiceError; @@ -7,8 +8,10 @@ use crate::error::ServiceError; #[async_trait] pub trait AuthenticableTrait { type User; - // fn password_valid(user: T); + type Session; async fn login(db: &DbConn, user: i32, password: String, ip_addr: String) -> Result<(String, String), ServiceError>; async fn auth(db: &DbConn, session_id: Uuid) -> Result; - async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError>; + async fn logout(db: &DbConn, session: Self::Session) -> Result<(), ServiceError>; + async fn new_session(db: &DbConn, user: Self::User, ip_addr: String, password: String) -> Result; + async fn delete_old_sessions(db: &DbConn, user: Self::User, keep_n_recent: usize) -> Result<(), ServiceError>; } \ No newline at end of file diff --git a/core/src/services/admin_service.rs b/core/src/services/admin_service.rs index c4114d1..4b53d73 100644 --- a/core/src/services/admin_service.rs +++ b/core/src/services/admin_service.rs @@ -1,10 +1,10 @@ use async_trait::async_trait; -use entity::admin; -use sea_orm::{prelude::Uuid, DbConn}; +use entity::{admin, admin_session}; +use sea_orm::{prelude::Uuid, DbConn, IntoActiveModel}; use crate::{crypto, error::ServiceError, Query, Mutation, models::auth::AuthenticableTrait}; -use super::session_service::{AdminUser, SessionService}; +use super::session_service::SessionService; pub struct AdminService; @@ -25,6 +25,7 @@ impl AdminService { #[async_trait] impl AuthenticableTrait for AdminService { type User = admin::Model; + type Session = admin_session::Model; async fn login( db: &DbConn, @@ -34,9 +35,8 @@ impl AuthenticableTrait for AdminService { ) -> Result<(String, String), ServiceError> { let admin = Query::find_admin_by_id(db, admin_id).await?.ok_or(ServiceError::InvalidCredentials)?; - let session_id = SessionService::new_session(db, - None, - Some(admin_id), + let session_id = Self::new_session(db, + admin.clone(), password.clone(), ip_addr ) @@ -47,16 +47,64 @@ impl AuthenticableTrait for AdminService { } async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { - match SessionService::auth_user_session(db, session_uuid).await? { - AdminUser::Admin(admin) => Ok(admin), - AdminUser::Candidate(_) => Err(ServiceError::Unauthorized), + let session = Query::find_admin_session_by_uuid(db, session_uuid) + .await? + .ok_or(ServiceError::Unauthorized)?; + + if !SessionService::is_valid(&session).await? { + Mutation::delete_session(db, session.into_active_model()).await?; + return Err(ServiceError::ExpiredSession); } + + let admin = Query::find_admin_by_id(db, session.admin_id.unwrap()) + .await? + .ok_or(ServiceError::CandidateNotFound)?; + + Ok(admin) } - async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> { - Mutation::delete_session(db, session_id).await?; + async fn logout(db: &DbConn, session: admin_session::Model) -> Result<(), ServiceError> { + Mutation::delete_session(db, session.into_active_model()).await?; Ok(()) } + + async fn new_session( + db: &DbConn, + admin: admin::Model, + password: String, + ip_addr: String, + ) -> Result { + if !crypto::verify_password(password.clone(), admin.password.clone()).await? { + return Err(ServiceError::InvalidCredentials); + } + // user is authenticated, generate a new session + let random_uuid: Uuid = Uuid::new_v4(); + + let session = Mutation::insert_admin_session(db, admin.id, random_uuid, ip_addr).await?; + + Self::delete_old_sessions(db, admin, 1) + .await?; + + Ok(session.id.to_string()) + } + async fn delete_old_sessions( + db: &DbConn, + admin: admin::Model, + keep_n_recent: usize, + ) -> Result<(), ServiceError> { + let mut sessions = Query::find_related_admin_sessions(db, admin) + .await?; + + sessions.sort_by_key(|s| s.created_at); + + let sessions = sessions.iter() + .map(|s| s.clone().into_active_model()) + .collect::>(); + + SessionService::delete_sessions(db, sessions, keep_n_recent).await?; + Ok(()) + } + } #[cfg(test)] diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 6d7a481..669a23c 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; -use entity::candidate; -use sea_orm::{prelude::Uuid, DbConn}; +use chrono::Duration; +use entity::{candidate, session}; +use sea_orm::{prelude::Uuid, DbConn, IntoActiveModel}; use crate::{ models::{candidate_details::{EncryptedApplicationDetails, EncryptedString, EncryptedCandidateDetails}, candidate::CandidateDetails}, @@ -9,7 +10,7 @@ use crate::{ Mutation, Query, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse}, auth::AuthenticableTrait}, utils::db::get_recipients, }; -use super::{session_service::{AdminUser, SessionService}, application_service::ApplicationService, portfolio_service::PortfolioService}; +use super::{session_service::SessionService, application_service::ApplicationService, portfolio_service::PortfolioService}; // TODO validation @@ -115,7 +116,7 @@ impl CandidateService { ).await?; - SessionService::revoke_all_sessions(db, Some(id), None).await?; + Self::delete_old_sessions(db, candidate.clone(), 0).await?; Mutation::update_candidate_password_and_keys(db, candidate.clone(), new_password_hash, pubkey, encrypted_priv_key).await?; // user might no have filled his details yet, but personal id number is filled from beginning @@ -223,11 +224,21 @@ impl CandidateService { let field_of_study_prefix = &s[0..3]; FIELD_OF_STUDY_PREFIXES.contains(&field_of_study_prefix) } + + pub async fn extend_session_duration_to_14_days(db: &DbConn, session: session::Model) -> Result<(), ServiceError> { + let now = chrono::Utc::now().naive_utc(); + if now >= session.updated_at.checked_add_signed(Duration::days(1)).ok_or(ServiceError::Unauthorized)? { + let new_expires_at = now.checked_add_signed(Duration::days(14)).ok_or(ServiceError::Unauthorized)?; + Mutation::update_session_expiration(db, session, new_expires_at).await?; + } + Ok(()) + } } #[async_trait] impl AuthenticableTrait for CandidateService { type User = candidate::Model; + type Session = session::Model; async fn login( db: &DbConn, @@ -239,7 +250,7 @@ impl AuthenticableTrait for CandidateService { .await? .ok_or(ServiceError::CandidateNotFound)?; - let session_id = SessionService::new_session(db, Some(application_id), None, password.clone(), ip_addr) + let session_id = Self::new_session(db, candidate.clone(), password.clone(), ip_addr) .await?; let private_key = Self::decrypt_private_key(candidate, password).await?; @@ -247,18 +258,68 @@ impl AuthenticableTrait for CandidateService { } async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { - match SessionService::auth_user_session(db, session_uuid).await { - Ok(user) => match user { - AdminUser::Candidate(candidate) => Ok(candidate), - AdminUser::Admin(_) => Err(ServiceError::Unauthorized), - }, - Err(e) => Err(e), + let session = Query::find_session_by_uuid(db, session_uuid) + .await? + .ok_or(ServiceError::Unauthorized)?; + + if !SessionService::is_valid(&session).await? { + Mutation::delete_session(db, session.into_active_model()).await?; + return Err(ServiceError::ExpiredSession); } + // Candidate authenticated + + Self::extend_session_duration_to_14_days(db, session.clone()).await?; + + let candidate = Query::find_candidate_by_id(db, session.candidate_id.unwrap()) + .await? + .ok_or(ServiceError::CandidateNotFound)?; + + Ok(candidate) } - async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> { - Mutation::delete_session(db, session_id).await?; + + async fn logout(db: &DbConn, session: session::Model) -> Result<(), ServiceError> { + Mutation::delete_session(db, session.into_active_model()).await?; Ok(()) } + + async fn new_session( + db: &DbConn, + candidate: candidate::Model, + password: String, + ip_addr: String, + ) -> Result { + if !crypto::verify_password(password.clone(), candidate.code.clone()).await? { + return Err(ServiceError::InvalidCredentials); + } + // user is authenticated, generate a new session + let random_uuid: Uuid = Uuid::new_v4(); + + let session = Mutation::insert_candidate_session(db, random_uuid, candidate.application, ip_addr).await?; + + Self::delete_old_sessions(db, candidate, 3) + .await + .ok(); + + Ok(session.id.to_string()) + } + async fn delete_old_sessions( + db: &DbConn, + candidate: candidate::Model, + keep_n_recent: usize, + ) -> Result<(), ServiceError> { + let mut sessions = Query::find_related_candidate_sessions(db, candidate) + .await?; + + sessions.sort_by_key(|s| s.created_at); + + let sessions = sessions.iter() + .map(|s| s.clone().into_active_model()) + .collect::>(); + + SessionService::delete_sessions(db, sessions, keep_n_recent).await?; + Ok(()) + } + } #[cfg(test)] diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index 0f3bb97..a977f25 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -1,13 +1,10 @@ use std::cmp::min; - -use chrono::Duration; -use entity::{admin, candidate, session}; -use sea_orm::{prelude::Uuid, ModelTrait, DbConn}; +use entity::{session_trait::UserSession}; +use sea_orm::{DbConn, ActiveModelTrait, ActiveModelBehavior}; use crate::{ - crypto::{self}, error::ServiceError, - Mutation, Query, + Mutation, }; pub enum AdminUser { @@ -18,134 +15,27 @@ pub enum AdminUser { pub(in crate::services) struct SessionService; impl SessionService { - /// Delete n old sessions for user - async fn delete_old_sessions( - db: &DbConn, - user_id: Option, - admin_id: Option, - keep_n_recent: usize, - ) -> Result<(), ServiceError> { - let mut sessions = Query::find_sessions_by_user_id(db, user_id, admin_id) - .await - .unwrap(); - - sessions.sort_by_key(|s| s.created_at); - - for session in sessions - .iter() - .take(sessions.len() - min(sessions.len(), keep_n_recent)) - { - Mutation::delete_session(db, session.id).await.unwrap(); - } - - Ok(()) - } - - /// Authenticate user by application id and password and generate a new session - pub async fn new_session( - db: &DbConn, - candidate_id: Option, - admin_id: Option, - password: String, - ip_addr: String, - ) -> Result { - if candidate_id.is_none() && admin_id.is_none() { - return Err(ServiceError::UserNotFoundBySessionId); - } - - if let Some(candidate_id) = candidate_id { - let candidate = Query::find_candidate_by_id(db, candidate_id).await? - .ok_or(ServiceError::CandidateNotFound)?; - - // compare passwords - if !crypto::verify_password(password.clone(), candidate.code.clone()).await? { - return Err(ServiceError::InvalidCredentials); - } - } - - if let Some(admin_id) = admin_id { - let admin = Query::find_admin_by_id(db, admin_id).await? - .ok_or(ServiceError::InvalidCredentials)?; - - // compare passwords - if !crypto::verify_password(password.clone(), admin.password.clone()).await? { - return Err(ServiceError::InvalidCredentials); - } - } - // user is authenticated, generate a new session - - let random_uuid: Uuid = Uuid::new_v4(); - - let session = Mutation::insert_session(db, candidate_id, admin_id, random_uuid, ip_addr).await?; - - SessionService::delete_old_sessions(db, candidate_id, admin_id, 3) - .await - .ok(); - - Ok(session.id.to_string()) - } - - pub async fn revoke_all_sessions(db: &DbConn, user_id: Option, admin_id: Option) -> Result<(), ServiceError> { - Self::delete_old_sessions(db, user_id, admin_id, 0).await - } - /// Check if session is valid - async fn is_valid(db: &DbConn, session: &session::Model) -> Result { + pub async fn is_valid(session: &T) -> Result where T: UserSession { let now = chrono::Utc::now().naive_utc(); - if now >= session.expires_at { - Mutation::delete_session(db, session.id).await?; + if now >= session.expires_at().await { Ok(false) } else { Ok(true) } - } - /// If 1 day or more since last update, extend session duration to 14 days - async fn extend_session_duration_to_14_days(db: &DbConn, session: session::Model) -> Result<(), ServiceError> { - let now = chrono::Utc::now().naive_utc(); - if now >= session.updated_at.checked_add_signed(Duration::days(1)).ok_or(ServiceError::Unauthorized)? { - let new_expires_at = now.checked_add_signed(Duration::days(14)).ok_or(ServiceError::Unauthorized)?; - Mutation::update_session_expiration(db, session, new_expires_at).await?; + /// Delete list of sessions + pub async fn delete_sessions(db: &DbConn, sessions: Vec, keep_n_recent: usize) -> Result<(), ServiceError> where T: ActiveModelTrait + std::marker::Send + ActiveModelBehavior { + for session in sessions + .iter() + .take(sessions.len() - min(sessions.len(), keep_n_recent)) + { + Mutation::delete_session(db, session.clone()).await?; } + Ok(()) - } - /// Authenticate user by session id - /// Return user model if session is valid - pub async fn auth_user_session( - db: &DbConn, - uuid: Uuid, - ) -> Result { - let session = Query::find_session_by_uuid(db, uuid).await? - .ok_or(ServiceError::UserNotFoundBySessionId)?; - - if !Self::is_valid(db, &session).await? { - return Err(ServiceError::ExpiredSession); - } - - Self::extend_session_duration_to_14_days(db, session.clone()).await?; - - let candidate = session.find_related(candidate::Entity).one(db).await; - let admin = session.find_related(admin::Entity).one(db).await; - - if candidate.is_err() || admin.is_err() { - eprintln!("Kurva"); - return Err(ServiceError::UserNotFoundBySessionId); - } - - if candidate.is_ok() { - if let Some(candidate) = candidate.unwrap() { - return Ok(AdminUser::Candidate(candidate)); - } - } - - if admin.is_ok() { - if let Some(admin) = admin.unwrap() { - return Ok(AdminUser::Admin(admin)); - } - } - return Err(ServiceError::UserNotFoundBySessionId); } } @@ -157,8 +47,8 @@ mod tests { use crate::{ crypto, - services::{application_service::ApplicationService, session_service::SessionService}, - utils::db::get_memory_sqlite_connection, + services::{application_service::ApplicationService, candidate_service::CandidateService}, + utils::db::get_memory_sqlite_connection, models::auth::AuthenticableTrait, }; #[tokio::test] @@ -190,7 +80,7 @@ mod tests { async fn test_candidate_session_correct_password() { let db = &get_memory_sqlite_connection().await; - ApplicationService::create_candidate_with_parent( + let candidate = ApplicationService::create_candidate_with_parent( db, 103151, &"Tajny_kod".to_string(), @@ -201,10 +91,9 @@ mod tests { .0; // correct password - let session = SessionService::new_session( + let session = CandidateService::new_session( db, - Some(103151), - None, + candidate, "Tajny_kod".to_string(), "127.0.0.1".to_string(), ) @@ -212,7 +101,7 @@ mod tests { .unwrap(); // println!("{}", session.err().unwrap().1); assert!( - SessionService::auth_user_session(db, Uuid::parse_str(&session).unwrap()) + CandidateService::auth(db, Uuid::parse_str(&session).unwrap()) .await .is_ok() ); @@ -233,10 +122,9 @@ mod tests { .0; // incorrect password - assert!(SessionService::new_session( + assert!(CandidateService::new_session( db, - Some(candidate_form.application), - None, + candidate_form, "Spatny_kod".to_string(), "127.0.0.1".to_string() ) diff --git a/entity/Cargo.toml b/entity/Cargo.toml index af3e744..f916167 100644 --- a/entity/Cargo.toml +++ b/entity/Cargo.toml @@ -10,6 +10,7 @@ path = "src/lib.rs" [dependencies] chrono = "^0.4" +async-trait = "0.1.60" [dependencies.sea-orm] version = "^0.10" diff --git a/entity/src/admin.rs b/entity/src/admin.rs index 8486730..3d06f08 100644 --- a/entity/src/admin.rs +++ b/entity/src/admin.rs @@ -18,13 +18,13 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { - #[sea_orm(has_many = "super::session::Entity")] - Session, + #[sea_orm(has_many = "super::admin_session::Entity")] + AdminSession, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::Session.def() + Relation::AdminSession.def() } } diff --git a/entity/src/admin_session.rs b/entity/src/admin_session.rs new file mode 100644 index 0000000..1815b5d --- /dev/null +++ b/entity/src/admin_session.rs @@ -0,0 +1,47 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + +use sea_orm::entity::prelude::*; + +use crate::session_trait::UserSession; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "admin_session")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub admin_id: Option, + pub ip_address: String, + pub created_at: DateTime, + pub expires_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::admin::Entity", + from = "Column::AdminId", + to = "super::admin::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Admin, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Admin.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[async_trait::async_trait] +impl UserSession for Model { + async fn id(&self) -> Uuid { + self.id + } + async fn expires_at(&self) -> chrono::NaiveDateTime { + self.expires_at + } +} diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index 8a1e76e..fdfbc46 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -30,16 +30,10 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { - #[sea_orm(has_many = "super::parent::Entity")] - Parent, #[sea_orm(has_many = "super::session::Entity")] Session, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Parent.def() - } + #[sea_orm(has_many = "super::parent::Entity")] + Parent, } impl Related for Entity { @@ -48,4 +42,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Parent.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/lib.rs b/entity/src/lib.rs index ef5f522..c2e3813 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -4,3 +4,5 @@ pub mod admin; pub mod candidate; pub mod parent; pub mod session; +pub mod admin_session; +pub mod session_trait; \ No newline at end of file diff --git a/entity/src/mod.rs b/entity/src/mod.rs index 63cf621..268951e 100644 --- a/entity/src/mod.rs +++ b/entity/src/mod.rs @@ -3,6 +3,7 @@ pub mod prelude; pub mod admin; +pub mod admin_session; pub mod candidate; pub mod parent; pub mod session; diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs index 73b608f..55797e7 100644 --- a/entity/src/prelude.rs +++ b/entity/src/prelude.rs @@ -1,6 +1,7 @@ //! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 pub use super::admin::Entity as Admin; +pub use super::admin_session::Entity as AdminSession; pub use super::candidate::Entity as Candidate; pub use super::parent::Entity as Parent; pub use super::session::Entity as Session; diff --git a/entity/src/session.rs b/entity/src/session.rs index 2d0599b..63efcde 100644 --- a/entity/src/session.rs +++ b/entity/src/session.rs @@ -2,13 +2,14 @@ use sea_orm::entity::prelude::*; +use crate::session_trait::UserSession; + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "session")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, - pub user_id: Option, - pub admin_id: Option, + pub candidate_id: Option, pub ip_address: String, pub created_at: DateTime, pub expires_at: DateTime, @@ -17,17 +18,9 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { - #[sea_orm( - belongs_to = "super::admin::Entity", - from = "Column::AdminId", - to = "super::admin::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Admin, #[sea_orm( belongs_to = "super::candidate::Entity", - from = "Column::UserId", + from = "Column::CandidateId", to = "super::candidate::Column::Application", on_update = "Cascade", on_delete = "Cascade" @@ -35,12 +28,6 @@ pub enum Relation { Candidate, } -impl Related for Entity { - fn to() -> RelationDef { - Relation::Admin.def() - } -} - impl Related for Entity { fn to() -> RelationDef { Relation::Candidate.def() @@ -48,3 +35,13 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} + +#[async_trait::async_trait] +impl UserSession for Model { + async fn id(&self) -> Uuid { + self.id + } + async fn expires_at(&self) -> chrono::NaiveDateTime { + self.expires_at + } +} \ No newline at end of file diff --git a/entity/src/session_trait.rs b/entity/src/session_trait.rs new file mode 100644 index 0000000..cec9db7 --- /dev/null +++ b/entity/src/session_trait.rs @@ -0,0 +1,8 @@ +use async_trait::async_trait; +use sea_orm::prelude::Uuid; + +#[async_trait] +pub trait UserSession { + async fn expires_at(&self) -> chrono::NaiveDateTime; + async fn id(&self) -> Uuid; +} \ No newline at end of file diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 6e2081d..3d8205f 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -8,6 +8,7 @@ mod m20221025_154422_create_session; mod m20221027_194728_session_create_user_fk; mod m20221028_194728_session_create_admin_fk; mod m20221112_112212_create_parent_candidate_fk; +mod m20221221_162232_create_admin_session; pub struct Migrator; #[async_trait::async_trait] @@ -20,8 +21,10 @@ impl MigratorTrait for Migrator { Box::new(m20221024_134454_insert_sample_admin::Migration::default()), Box::new(m20221025_154422_create_session::Migration), Box::new(m20221027_194728_session_create_user_fk::Migration), - Box::new(m20221028_194728_session_create_admin_fk::Migration), Box::new(m20221112_112212_create_parent_candidate_fk::Migration), + Box::new(m20221221_162232_create_admin_session::Migration), + Box::new(m20221028_194728_session_create_admin_fk::Migration), + ] } } diff --git a/migration/src/m20221025_154422_create_session.rs b/migration/src/m20221025_154422_create_session.rs index c3e06e3..94cd12a 100644 --- a/migration/src/m20221025_154422_create_session.rs +++ b/migration/src/m20221025_154422_create_session.rs @@ -18,8 +18,7 @@ impl MigrationTrait for Migration { .unique_key() .primary_key(), ) - .col(ColumnDef::new(Session::UserId).integer()) - .col(ColumnDef::new(Session::AdminId).integer()) + .col(ColumnDef::new(Session::CandidateId).integer()) .col(ColumnDef::new(Session::IpAddress).string().not_null()) .col(ColumnDef::new(Session::CreatedAt).date_time().not_null()) .col(ColumnDef::new(Session::ExpiresAt).date_time().not_null()) @@ -40,8 +39,7 @@ impl MigrationTrait for Migration { pub enum Session { Table, Id, - UserId, - AdminId, + CandidateId, IpAddress, CreatedAt, ExpiresAt, diff --git a/migration/src/m20221027_194728_session_create_user_fk.rs b/migration/src/m20221027_194728_session_create_user_fk.rs index 248fe2d..9a06760 100644 --- a/migration/src/m20221027_194728_session_create_user_fk.rs +++ b/migration/src/m20221027_194728_session_create_user_fk.rs @@ -10,7 +10,7 @@ impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager.create_foreign_key(ForeignKey::create() .name("user_fk") - .from(Session::Table, Session::UserId) + .from(Session::Table, Session::CandidateId) .to(Candidate::Table, Candidate::Application) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade) diff --git a/migration/src/m20221028_194728_session_create_admin_fk.rs b/migration/src/m20221028_194728_session_create_admin_fk.rs index 5b36851..214b064 100644 --- a/migration/src/m20221028_194728_session_create_admin_fk.rs +++ b/migration/src/m20221028_194728_session_create_admin_fk.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -use crate::{m20221025_154422_create_session::Session, m20221024_111310_create_admin::Admin}; +use crate::{m20221221_162232_create_admin_session::AdminSession, m20221024_111310_create_admin::Admin}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -10,7 +10,7 @@ impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager.create_foreign_key(ForeignKey::create() .name("admin_fk") - .from(Session::Table, Session::AdminId) + .from(AdminSession::Table, AdminSession::AdminId) .to(Admin::Table, Admin::Id) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade) @@ -20,7 +20,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager.drop_foreign_key(ForeignKey::drop() .name("admin_fk") - .table(Session::Table) + .table(AdminSession::Table) .to_owned()).await } } \ No newline at end of file diff --git a/migration/src/m20221221_162232_create_admin_session.rs b/migration/src/m20221221_162232_create_admin_session.rs new file mode 100644 index 0000000..73d343d --- /dev/null +++ b/migration/src/m20221221_162232_create_admin_session.rs @@ -0,0 +1,48 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(AdminSession::Table) + .if_not_exists() + .col( + ColumnDef::new(AdminSession::Id) + .uuid() + .unique_key() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(AdminSession::AdminId).integer()) + .col(ColumnDef::new(AdminSession::IpAddress).string().not_null()) + .col(ColumnDef::new(AdminSession::CreatedAt).date_time().not_null()) + .col(ColumnDef::new(AdminSession::ExpiresAt).date_time().not_null()) + .col(ColumnDef::new(AdminSession::UpdatedAt).date_time().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(AdminSession::Table).to_owned()) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +pub enum AdminSession { + Table, + Id, + AdminId, + IpAddress, + CreatedAt, + ExpiresAt, + UpdatedAt, +} From 304c4ddfea683f647e335b5a83ca929903f9a25c Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Wed, 21 Dec 2022 18:49:45 +0100 Subject: [PATCH 06/10] fix: sqlite testing --- core/src/utils/db.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/utils/db.rs b/core/src/utils/db.rs index c9a4503..ac1b4bd 100644 --- a/core/src/utils/db.rs +++ b/core/src/utils/db.rs @@ -1,3 +1,4 @@ +use entity::admin_session; use sea_orm::DbConn; use crate::Query; @@ -24,10 +25,12 @@ pub async fn get_memory_sqlite_connection() -> sea_orm::DbConn { let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity); let stmt3: TableCreateStatement = schema.create_table_from_entity(session::Entity); let stmt4: TableCreateStatement = schema.create_table_from_entity(parent::Entity); + let stmt5: TableCreateStatement = schema.create_table_from_entity(admin_session::Entity); db.execute(db.get_database_backend().build(&stmt)).await.unwrap(); db.execute(db.get_database_backend().build(&stmt2)).await.unwrap(); db.execute(db.get_database_backend().build(&stmt3)).await.unwrap(); db.execute(db.get_database_backend().build(&stmt4)).await.unwrap(); + db.execute(db.get_database_backend().build(&stmt5)).await.unwrap(); db } From eba448951809af63fc494b5f6ec36b7a4712e9f7 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 22 Dec 2022 11:00:14 +0100 Subject: [PATCH 07/10] fix: delete unused AdminUser enum --- core/src/services/session_service.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index a977f25..16e05c6 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -7,11 +7,6 @@ use crate::{ Mutation, }; -pub enum AdminUser { - Admin(entity::admin::Model), - Candidate(entity::candidate::Model), -} - pub(in crate::services) struct SessionService; impl SessionService { From da538a50b3b23708aa4168bc4663de553bd116c7 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 22 Dec 2022 11:00:24 +0100 Subject: [PATCH 08/10] chore: async-trait in Cargo.lock --- Cargo.lock | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23be0b9..1816c81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,9 +226,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" dependencies = [ "proc-macro2", "quote", @@ -2064,6 +2064,7 @@ dependencies = [ "argon2", "async-compat", "async-tempfile", + "async-trait", "async_zip", "base64", "chrono", @@ -2088,6 +2089,7 @@ dependencies = [ name = "portfolio-entity" version = "0.1.0" dependencies = [ + "async-trait", "chrono", "sea-orm", ] From a96c0ce9f78e859f5c448e4b766f58011d3575c6 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 22 Dec 2022 11:37:01 +0100 Subject: [PATCH 09/10] feat: code cleanup --- core/src/database/query/session.rs | 11 ++++++++--- core/src/services/admin_service.rs | 14 +++++--------- core/src/services/candidate_service.rs | 18 ++++++------------ 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs index ba2eceb..d7b2b92 100644 --- a/core/src/database/query/session.rs +++ b/core/src/database/query/session.rs @@ -22,11 +22,17 @@ impl Query { } pub async fn find_related_candidate_sessions(db: &DbConn, candidate: candidate::Model) -> Result, DbErr> { - candidate.find_related(Session).all(db).await + candidate.find_related(Session) + .order_by_asc(session::Column::UpdatedAt) + .all(db) + .await } pub async fn find_related_admin_sessions(db: &DbConn, admin: admin::Model) -> Result, DbErr> { - admin.find_related(admin_session::Entity).all(db).await + admin.find_related(admin_session::Entity) + .order_by_asc(admin_session::Column::UpdatedAt) + .all(db) + .await } // find session by user id @@ -50,7 +56,6 @@ impl Query { #[cfg(test)] mod tests { use entity::{session, admin, candidate, admin_session}; - use sea_orm::ActiveValue::NotSet; use sea_orm::{prelude::Uuid, ActiveModelTrait, Set}; use crate::utils::db::get_memory_sqlite_connection; diff --git a/core/src/services/admin_service.rs b/core/src/services/admin_service.rs index 4b53d73..6d30950 100644 --- a/core/src/services/admin_service.rs +++ b/core/src/services/admin_service.rs @@ -82,8 +82,7 @@ impl AuthenticableTrait for AdminService { let session = Mutation::insert_admin_session(db, admin.id, random_uuid, ip_addr).await?; - Self::delete_old_sessions(db, admin, 1) - .await?; + Self::delete_old_sessions(db, admin, 1).await?; Ok(session.id.to_string()) } @@ -92,14 +91,11 @@ impl AuthenticableTrait for AdminService { admin: admin::Model, keep_n_recent: usize, ) -> Result<(), ServiceError> { - let mut sessions = Query::find_related_admin_sessions(db, admin) - .await?; - - sessions.sort_by_key(|s| s.created_at); - - let sessions = sessions.iter() + let sessions = Query::find_related_admin_sessions(db, admin) + .await? + .iter() .map(|s| s.clone().into_active_model()) - .collect::>(); + .collect(); SessionService::delete_sessions(db, sessions, keep_n_recent).await?; Ok(()) diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 669a23c..b1f0bab 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -250,8 +250,7 @@ impl AuthenticableTrait for CandidateService { .await? .ok_or(ServiceError::CandidateNotFound)?; - let session_id = Self::new_session(db, candidate.clone(), password.clone(), ip_addr) - .await?; + let session_id = Self::new_session(db, candidate.clone(), password.clone(), ip_addr).await?; let private_key = Self::decrypt_private_key(candidate, password).await?; Ok((session_id, private_key)) @@ -296,9 +295,7 @@ impl AuthenticableTrait for CandidateService { let session = Mutation::insert_candidate_session(db, random_uuid, candidate.application, ip_addr).await?; - Self::delete_old_sessions(db, candidate, 3) - .await - .ok(); + Self::delete_old_sessions(db, candidate, 3).await?; Ok(session.id.to_string()) } @@ -307,14 +304,11 @@ impl AuthenticableTrait for CandidateService { candidate: candidate::Model, keep_n_recent: usize, ) -> Result<(), ServiceError> { - let mut sessions = Query::find_related_candidate_sessions(db, candidate) - .await?; - - sessions.sort_by_key(|s| s.created_at); - - let sessions = sessions.iter() + let sessions = Query::find_related_candidate_sessions(db, candidate) + .await? + .iter() .map(|s| s.clone().into_active_model()) - .collect::>(); + .collect(); SessionService::delete_sessions(db, sessions, keep_n_recent).await?; Ok(()) From fdce475fd81be712b79a385042c03b018b563d33 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 22 Dec 2022 11:38:28 +0100 Subject: [PATCH 10/10] style: delete comment --- core/src/database/query/session.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs index d7b2b92..8d5b753 100644 --- a/core/src/database/query/session.rs +++ b/core/src/database/query/session.rs @@ -34,23 +34,6 @@ impl Query { .all(db) .await } - - // find session by user id - /* pub async fn find_sessions_by_user_id( - db: &DbConn, - user_id: Option, - admin_id: Option, - ) -> Result, DbErr> { - if user_id.is_some() { - Session::find() - .filter(session::Column::UserId.eq(user_id)) - } else { - Session::find() - .filter(session::Column::AdminId.eq(admin_id)) - } - .all(db) - .await - } */ } #[cfg(test)]