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();