diff --git a/api/src/lib.rs b/api/src/lib.rs index 22a905d..15d65ca 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -68,8 +68,14 @@ async fn start() -> Result<(), rocket::Error> { routes::admin::whoami, routes::admin::hello, routes::admin::create_candidate, + routes::admin::get_candidate, ], ) + .mount( + "/admin/list", + routes![ + routes::admin::list_candidates, + ]) .register("/", catchers![]) .launch() .await diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index b57261f..5c90006 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use portfolio_core::{ crypto::random_8_char_string, - services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService}, + services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService}, responses::CandidateResponse, candidate_details::ApplicationDetails, }; use requests::{AdminLoginRequest, RegisterRequest}; use rocket::http::{Cookie, Status, CookieJar}; @@ -86,3 +86,39 @@ pub async fn create_candidate( Ok(plain_text_password) } + +#[get("/candidates?")] +pub async fn list_candidates( + conn: Connection<'_, Db>, + session: AdminAuth, + field: Option, +) -> Result>, Custom> { + let db = conn.into_inner(); + let private_key = session.get_private_key(); + + let candidates = CandidateService::list_candidates(private_key, db, field) + .await + .map_err(|e| Custom(Status::from_code(e.code()).unwrap(), e.to_string()))?; + + Ok(Json(candidates)) +} + +#[get("/candidate/")] +pub async fn get_candidate( + conn: Connection<'_, Db>, + session: AdminAuth, + id: i32, +) -> Result, Custom> { + let db = conn.into_inner(); + let private_key = session.get_private_key(); + + let details = ApplicationService::decrypt_all_details( + private_key, + db, + id + ) + .await + .map_err(|e| Custom(Status::from_code(e.code()).unwrap(), e.to_string()))?; + + Ok(Json(details)) +} \ No newline at end of file diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index 3d2dcd2..d3aa1f3 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -12,7 +12,6 @@ use sea_orm_rocket::Connection; use crate::guards::data::letter::Letter; use crate::guards::data::portfolio::Portfolio; -use crate::requests::PasswordRequest; use crate::{guards::request::auth::CandidateAuth, pool::Db, requests}; #[post("/login", data = "")] @@ -83,18 +82,21 @@ pub async fn add_details( Ok("Details added".to_string()) } -#[post("/get_details", data = "")] +#[post("/get_details")] pub async fn get_details( conn: Connection<'_, Db>, - password_form: Json, - session: CandidateAuth, + session: CandidateAuth ) -> Result, Custom> { let db = conn.into_inner(); + let private_key = session.get_private_key(); let candidate: entity::candidate::Model = session.into(); - let password = password_form.password.clone(); + // let handle = tokio::spawn(async move { - let details = ApplicationService::decrypt_all_details(db, candidate.application, password) + let details = ApplicationService::decrypt_all_details(private_key, + db, + candidate.application + ) .await .map_err(|e| { Custom( diff --git a/core/src/candidate_details.rs b/core/src/candidate_details.rs index dc6350a..ae3cb4f 100644 --- a/core/src/candidate_details.rs +++ b/core/src/candidate_details.rs @@ -185,6 +185,7 @@ impl TryFrom<(candidate::Model, parent::Model)> for EncryptedApplicationDetails } } + #[derive(Debug, Serialize, Deserialize)] pub struct ApplicationDetails { // Candidate diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index 2a6d7c7..37478b9 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -1,7 +1,23 @@ -use crate::Query; +use crate::{Query}; -use ::entity::{candidate, candidate::Entity as Candidate}; +use ::entity::{candidate, candidate::Entity as Candidate, parent}; use sea_orm::*; +use serde::Serialize; + + +pub const PAGE_SIZE: u64 = 20; + +#[derive(FromQueryResult, Serialize)] +pub struct ApplicationResult { + pub application: i32, + pub name: Option, + pub surname: Option, + pub study: Option, + pub citizenship: Option, + + pub parent_name: Option, + pub parent_surname: Option, +} impl Query { pub async fn find_candidate_by_id( @@ -10,6 +26,21 @@ impl Query { ) -> Result, DbErr> { Candidate::find_by_id(id).one(db).await } + + pub async fn list_candidates( + db: &DbConn, + field_of_study: Option, + ) -> Result, DbErr> { + Candidate::find() + .join(JoinType::InnerJoin, candidate::Relation::Parent.def()) + .column_as(parent::Column::Name, "parent_name") + .column_as(parent::Column::Surname, "parent_surname") + .into_model::() + .paginate(db, PAGE_SIZE) + .fetch() + .await + } + } #[cfg(test)] diff --git a/core/src/lib.rs b/core/src/lib.rs index 322409c..579ddcd 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -5,6 +5,7 @@ pub mod services; pub mod error; pub mod candidate_details; pub mod util; +pub mod responses; pub use database::mutation::*; pub use database::query::*; diff --git a/core/src/responses.rs b/core/src/responses.rs new file mode 100644 index 0000000..c2cbdd2 --- /dev/null +++ b/core/src/responses.rs @@ -0,0 +1,47 @@ +use serde::Serialize; + +use crate::{candidate_details::EncryptedString, error::ServiceError}; + +#[derive(Debug, Serialize)] +pub struct CandidateResponse { + pub application_id: i32, + pub name: String, + pub surname: String, + pub study: String, + pub submitted: bool, +} + +impl CandidateResponse { + pub async fn from_encrypted( + private_key: &String, + application_id: i32, + name_opt: Option, + surname_opt: Option, + study_opt: Option, + submitted: bool, + ) -> Result { + let name = decrypt_if_exists(private_key, name_opt).await?; + let surname = decrypt_if_exists(private_key, surname_opt).await?; + let study = decrypt_if_exists(private_key, study_opt).await?; + Ok( + Self { + application_id, + name, + surname, + study, + submitted, + } + ) + } + +} + +async fn decrypt_if_exists( + private_key: &String, + encrypted_string: Option, +) -> Result { + match EncryptedString::try_from(encrypted_string) { + Ok(encrypted_string) => Ok(encrypted_string.decrypt(private_key).await?), + Err(_) => Ok(String::from("")), + } +} \ No newline at end of file diff --git a/core/src/services/admin_service.rs b/core/src/services/admin_service.rs index 71bda95..76c719a 100644 --- a/core/src/services/admin_service.rs +++ b/core/src/services/admin_service.rs @@ -13,19 +13,9 @@ impl AdminService { admin_id: i32, password: String, ) -> Result { - let admin = Query::find_admin_by_id(db, admin_id).await?; - - let Some(admin) = admin else { - return Err(ServiceError::CandidateNotFound); - }; - + let admin = Query::find_admin_by_id(db, admin_id).await?.ok_or(ServiceError::InvalidCredentials)?; let private_key_encrypted = admin.private_key; - - let private_key = crypto::decrypt_password(private_key_encrypted, password).await; - - let Ok(private_key) = private_key else { - return Err(ServiceError::CryptoDecryptFailed); - }; + let private_key = crypto::decrypt_password(private_key_encrypted, password).await?; Ok(private_key) } @@ -36,24 +26,64 @@ impl AdminService { password: String, ip_addr: String, ) -> Result<(String, String), ServiceError> { - let session_id = - SessionService::new_session(db, None, Some(admin_id), password.clone(), ip_addr).await; - match session_id { - Ok(session_id) => { - let private_key = Self::decrypt_private_key(db, admin_id, password).await?; - Ok((session_id, private_key)) - } - Err(e) => Err(e), - } + let session_id = SessionService::new_session(db, + None, + Some(admin_id), + password.clone(), + ip_addr + ) + .await?; + + let private_key = Self::decrypt_private_key(db, admin_id, 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::Admin(admin) => Ok(admin), - AdminUser::Candidate(_) => unreachable!(), - }, - Err(e) => Err(e), + match SessionService::auth_user_session(db, session_uuid).await? { + AdminUser::Admin(admin) => Ok(admin), + AdminUser::Candidate(_) => unreachable!(), } } } + +#[cfg(test)] +mod admin_tests { + use chrono::Local; + use entity::admin; + use sea_orm::{Set, ActiveModelTrait}; + + use crate::{util::get_memory_sqlite_connection, error::ServiceError}; + + use super::*; + + + #[tokio::test] + async fn test_admin_login() -> Result<(), ServiceError> { + let db = get_memory_sqlite_connection().await; + let _ = admin::ActiveModel { + id: Set(1), + name: Set("Admin".to_owned()), + public_key: Set("age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_owned()), + // AGE-SECRET-KEY-14QG24502DMUUQDT2SPMX2YXPSES0X8UD6NT0PCTDAT6RH8V5Q3GQGSRXPS + private_key: Set("5KCEGk0ueWVGnu5Xo3rmpLoilcVZ2ZWmwIcdZEJ8rrBNW7jwzZU/XTcTXtk/xyy/zjF8s+YnuVpOklQvX3EC/Sn+ZwyPY3jokM2RNwnZZlnqdehOEV1SMm/Y".to_owned()), + // test + password: Set("$argon2i$v=19$m=6000,t=3,p=10$WE9xCQmmWdBK82R4SEjoqA$TZSc6PuLd4aWK2x2WAb+Lm9sLySqjK3KLbNyqyQmzPQ".to_owned()), + created_at: Set(Local::now().naive_local()), + updated_at: Set(Local::now().naive_local()), + ..Default::default() + } + .insert(&db) + .await?; + + let (session_id, _private_key) = AdminService::login(&db, 1, "test".to_owned(), "127.0.0.1".to_owned()).await?; + + let logged_admin = AdminService::auth(&db, session_id.parse().unwrap()).await?; + + assert_eq!(logged_admin.id, 1); + assert_eq!(logged_admin.name, "Admin"); + + + Ok(()) + + } +} \ No newline at end of file diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 048e404..796d8b0 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -60,32 +60,17 @@ impl ApplicationService { } pub async fn decrypt_all_details( + private_key: String, db: &DbConn, application_id: i32, - password: String, ) -> Result { - let candidate = match Query::find_candidate_by_id(db, application_id).await { - Ok(candidate) => candidate.unwrap(), - Err(e) => return Err(ServiceError::DbError(e)), // TODO: logging - }; - let parent = Query::find_parent_by_id(db, application_id).await?.unwrap(); - - match crypto::verify_password((&password).to_string(), candidate.code.clone()).await { - Ok(valid) => { - if !valid { - return Err(ServiceError::InvalidCredentials); - } - } - Err(_) => return Err(ServiceError::InvalidCredentials), - } - - let dec_priv_key = crypto::decrypt_password(candidate.private_key.clone(), password) - .await - .ok() - .unwrap(); + let candidate = Query::find_candidate_by_id(db, application_id).await? + .ok_or(ServiceError::CandidateNotFound)?; + let parent = Query::find_parent_by_id(db, application_id).await? + .ok_or(ServiceError::ParentNotFound)?; let enc_details = EncryptedApplicationDetails::try_from((candidate, parent))?; - enc_details.decrypt(dec_priv_key).await + enc_details.decrypt(private_key).await } } \ No newline at end of file diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index f4ff3b1..9deacb1 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -5,14 +5,42 @@ use sea_orm::{prelude::Uuid, DbConn}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ - candidate_details::EncryptedApplicationDetails, + candidate_details::{EncryptedApplicationDetails}, crypto::{self, hash_password}, error::ServiceError, - Mutation, Query, + Mutation, Query, responses::CandidateResponse, }; use super::session_service::{AdminUser, SessionService}; +// TODO + +/* pub struct FieldOfStudy { + pub short_name: String, + pub full_name: String, + pub code: i32, +} + +impl FieldOfStudy { + pub fn new(short_name: String, full_name: String, code: i32) -> Self { + Self { + short_name, + full_name, + code, + } + } + + pub fn code_str(&self) -> String { + format!("{:04}", self.code) + } +} + +pub enum FieldsOfStudy { + KB(FieldOfStudy), + IT(FieldOfStudy), + G(FieldOfStudy), +} */ + const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; pub struct CandidateService; @@ -81,6 +109,31 @@ impl CandidateService { Ok(model) } + pub async fn list_candidates( + private_key: String, + db: &DbConn, + field_of_study: Option, + ) -> Result, ServiceError> { + + let candidates = Query::list_candidates(db, None).await?; + let mut result: Vec = vec![]; + + for candidate in candidates { + result.push( + CandidateResponse::from_encrypted( + &private_key, + candidate.application, + candidate.name, + candidate.surname, + candidate.study, + true + ).await? + ) + } + + Ok(result) + } + pub fn is_candidate_info(candidate: &candidate::Model) -> bool { candidate.name.is_some() && candidate.surname.is_some() @@ -370,6 +423,19 @@ mod tests { assert!(!CandidateService::is_application_id_valid(101)); } + // TODO + /* #[tokio::test] + async fn test_list_candidates() { + let db = get_memory_sqlite_connection().await; + let candidates = CandidateService::list_candidates(&db, None).await.unwrap(); + assert_eq!(candidates.len(), 0); + + put_user_data(&db).await; + + let candidates = CandidateService::list_candidates(&db, None).await.unwrap(); + assert_eq!(candidates.len(), 1); + } */ + #[tokio::test] async fn test_encrypt_decrypt_private_key_with_passphrase() { let db = get_memory_sqlite_connection().await; @@ -426,7 +492,7 @@ mod tests { citizenship: "test".to_string(), email: "test".to_string(), sex: "test".to_string(), - study: "test".to_string(), + study: "KB".to_string(), parent_name: "test".to_string(), parent_surname: "test".to_string(), parent_telephone: "test".to_string(), diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index f81cc3e..4960fc4 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -162,7 +162,12 @@ impl SessionService { #[cfg(test)] mod tests { - use sea_orm::prelude::Uuid; + use entity::{admin, candidate, session, parent}; + + use sea_orm::{ + prelude::Uuid, sea_query::TableCreateStatement, ConnectionTrait, Database, DbBackend, + DbConn, Schema, + }; use crate::{ crypto,