mirror of
https://github.com/danbulant/Portfolio
synced 2026-06-08 09:12:26 +00:00
commit
25b403c909
12 changed files with 111 additions and 39 deletions
|
|
@ -150,9 +150,16 @@ pub fn rocket() -> Rocket<Build> {
|
||||||
routes::admin::get_candidate,
|
routes::admin::get_candidate,
|
||||||
routes::admin::reset_candidate_password,
|
routes::admin::reset_candidate_password,
|
||||||
routes::admin::get_candidate_portfolio,
|
routes::admin::get_candidate_portfolio,
|
||||||
|
routes::admin::delete_candidate,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.mount("/admin/list", routes![routes::admin::list_candidates,])
|
.mount(
|
||||||
|
"/admin/list",
|
||||||
|
routes![
|
||||||
|
routes::admin::list_candidates,
|
||||||
|
routes::admin::list_candidates_csv,
|
||||||
|
]
|
||||||
|
)
|
||||||
.register("/", catchers![])
|
.register("/", catchers![])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::net::{SocketAddr, IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
use portfolio_core::{
|
use portfolio_core::{
|
||||||
crypto::random_8_char_string,
|
crypto::random_8_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,
|
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,
|
||||||
};
|
};
|
||||||
use requests::{AdminLoginRequest, RegisterRequest};
|
use requests::{AdminLoginRequest, RegisterRequest};
|
||||||
use rocket::http::{Cookie, Status, CookieJar};
|
use rocket::http::{Cookie, Status, CookieJar};
|
||||||
|
|
@ -59,7 +59,7 @@ pub async fn logout(conn: Connection<'_, Db>, _session: AdminAuth, cookies: &Coo
|
||||||
let session_id = Uuid::try_parse(cookie.value()) // unwrap would be safe here because of the auth guard
|
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()))?;
|
.map_err(|e| Custom(Status::BadRequest, e.to_string()))?;
|
||||||
|
|
||||||
let res = AdminService::logout(db, session_id)
|
let _res = AdminService::logout(db, session_id)
|
||||||
.await
|
.await
|
||||||
.map_err(to_custom_error)?;
|
.map_err(to_custom_error)?;
|
||||||
|
|
||||||
|
|
@ -137,6 +137,23 @@ pub async fn list_candidates(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/candidates_csv")]
|
||||||
|
pub async fn list_candidates_csv(
|
||||||
|
conn: Connection<'_, Db>,
|
||||||
|
session: AdminAuth,
|
||||||
|
) -> Result<Vec<u8>, Custom<String>> {
|
||||||
|
let db = conn.into_inner();
|
||||||
|
let private_key = session.get_private_key();
|
||||||
|
|
||||||
|
let candidates = csv::export(db, private_key)
|
||||||
|
.await
|
||||||
|
.map_err(to_custom_error)?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
candidates
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/candidate/<id>")]
|
#[get("/candidate/<id>")]
|
||||||
pub async fn get_candidate(
|
pub async fn get_candidate(
|
||||||
conn: Connection<'_, Db>,
|
conn: Connection<'_, Db>,
|
||||||
|
|
@ -164,6 +181,24 @@ pub async fn get_candidate(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[delete("/candidate/<id>")]
|
||||||
|
pub async fn delete_candidate(
|
||||||
|
conn: Connection<'_, Db>,
|
||||||
|
_session: AdminAuth,
|
||||||
|
id: i32,
|
||||||
|
) -> Result<(), Custom<String>> {
|
||||||
|
let db = conn.into_inner();
|
||||||
|
|
||||||
|
let candidate = Query::find_candidate_by_id(db, id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| to_custom_error(ServiceError::DbError(e)))?
|
||||||
|
.ok_or(to_custom_error(ServiceError::CandidateNotFound))?;
|
||||||
|
|
||||||
|
CandidateService::delete_candidate(db, candidate)
|
||||||
|
.await
|
||||||
|
.map_err(to_custom_error)
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/candidate/<id>/reset_password")]
|
#[post("/candidate/<id>/reset_password")]
|
||||||
pub async fn reset_candidate_password(
|
pub async fn reset_candidate_password(
|
||||||
conn: Connection<'_, Db>,
|
conn: Connection<'_, Db>,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,17 @@ impl Mutation {
|
||||||
Ok(insert)
|
Ok(insert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_candidate(
|
||||||
|
db: &DbConn,
|
||||||
|
candidate: candidate::Model,
|
||||||
|
) -> Result<DeleteResult, DbErr> {
|
||||||
|
let application = candidate.application;
|
||||||
|
let delete = candidate.delete(db).await?;
|
||||||
|
|
||||||
|
warn!("CANDIDATE {} DELETED", application);
|
||||||
|
Ok(delete)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_candidate_password_and_keys(
|
pub async fn update_candidate_password_and_keys(
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
candidate: candidate::Model,
|
candidate: candidate::Model,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use sea_orm::*;
|
||||||
|
|
||||||
use ::entity::{candidate, candidate::Entity as Candidate, parent};
|
use ::entity::{candidate, candidate::Entity as Candidate, parent};
|
||||||
|
|
||||||
use crate::{Query, models::candidate::CandidateWithParent};
|
use crate::Query;
|
||||||
|
|
||||||
pub const PAGE_SIZE: u64 = 20;
|
pub const PAGE_SIZE: u64 = 20;
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ impl Query {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_candidates(
|
pub async fn list_candidates_preview(
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
field_of_study_opt: Option<String>,
|
field_of_study_opt: Option<String>,
|
||||||
page: Option<u64>,
|
page: Option<u64>,
|
||||||
|
|
@ -64,18 +64,11 @@ impl Query {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_candidates_full(
|
||||||
pub async fn list_all_candidates_with_parents(
|
db: &DbConn
|
||||||
db: &DbConn,
|
) -> Result<Vec<candidate::Model>, DbErr> {
|
||||||
) -> Result<Vec<CandidateWithParent>, DbErr> {
|
|
||||||
Candidate::find()
|
Candidate::find()
|
||||||
.order_by(candidate::Column::Application, Order::Asc)
|
.order_by(candidate::Column::Application, Order::Asc)
|
||||||
.join(JoinType::InnerJoin, candidate::Relation::Parent.def())
|
|
||||||
.column_as(parent::Column::Name, "parent_name")
|
|
||||||
.column_as(parent::Column::Surname, "parent_surname")
|
|
||||||
.column_as(parent::Column::Telephone, "parent_telephone")
|
|
||||||
.column_as(parent::Column::Email, "parent_email")
|
|
||||||
.into_model::<CandidateWithParent>()
|
|
||||||
.all(db)
|
.all(db)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ impl Query {
|
||||||
|
|
||||||
pub async fn find_candidate_parents(
|
pub async fn find_candidate_parents(
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
candidate: candidate::Model,
|
candidate: &candidate::Model,
|
||||||
) -> Result<Vec<Model>, DbErr> {
|
) -> Result<Vec<Model>, DbErr> {
|
||||||
|
|
||||||
candidate.find_related(parent::Entity)
|
candidate.find_related(parent::Entity)
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ pub struct ApplicationDetails {
|
||||||
/// CSV export (admin endpoint)
|
/// CSV export (admin endpoint)
|
||||||
#[derive(FromQueryResult, Serialize, Default)]
|
#[derive(FromQueryResult, Serialize, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CandidateWithParent {
|
pub struct Row {
|
||||||
pub application: i32,
|
pub application: i32,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub surname: Option<String>,
|
pub surname: Option<String>,
|
||||||
|
|
@ -81,6 +81,11 @@ pub struct CandidateWithParent {
|
||||||
pub parent_surname: Option<String>,
|
pub parent_surname: Option<String>,
|
||||||
pub parent_telephone: Option<String>,
|
pub parent_telephone: Option<String>,
|
||||||
pub parent_email: Option<String>,
|
pub parent_email: Option<String>,
|
||||||
|
|
||||||
|
pub second_parent_name: Option<String>,
|
||||||
|
pub second_parent_surname: Option<String>,
|
||||||
|
pub second_parent_telephone: Option<String>,
|
||||||
|
pub second_parent_email: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BaseCandidateResponse {
|
impl BaseCandidateResponse {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use chrono::NaiveDate;
|
||||||
use entity::{candidate, parent};
|
use entity::{candidate, parent};
|
||||||
use futures::future;
|
use futures::future;
|
||||||
|
|
||||||
use crate::{crypto, models::candidate::{CandidateWithParent, ApplicationDetails}, error::ServiceError};
|
use crate::{crypto, models::candidate::{Row, ApplicationDetails}, error::ServiceError};
|
||||||
|
|
||||||
use super::candidate::{CandidateDetails, ParentDetails};
|
use super::candidate::{CandidateDetails, ParentDetails};
|
||||||
|
|
||||||
|
|
@ -290,11 +290,11 @@ impl TryFrom<(candidate::Model, Vec<parent::Model>)> for EncryptedApplicationDet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<CandidateWithParent> for EncryptedApplicationDetails {
|
impl TryFrom<Row> for EncryptedApplicationDetails {
|
||||||
type Error = ServiceError;
|
type Error = ServiceError;
|
||||||
|
|
||||||
fn try_from(
|
fn try_from(
|
||||||
cp: CandidateWithParent,
|
cp: Row,
|
||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
Ok(EncryptedApplicationDetails {
|
Ok(EncryptedApplicationDetails {
|
||||||
candidate: EncryptedCandidateDetails {
|
candidate: EncryptedCandidateDetails {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ impl ApplicationService {
|
||||||
candidate: candidate::Model,
|
candidate: candidate::Model,
|
||||||
// parents: Vec<parent::Model>,
|
// parents: Vec<parent::Model>,
|
||||||
) -> Result<ApplicationDetails, ServiceError> {
|
) -> Result<ApplicationDetails, ServiceError> {
|
||||||
let parents = Query::find_candidate_parents(db, candidate.clone()).await?;
|
let parents = Query::find_candidate_parents(db, &candidate).await?;
|
||||||
let enc_details = EncryptedApplicationDetails::try_from((candidate, parents))?;
|
let enc_details = EncryptedApplicationDetails::try_from((candidate, parents))?;
|
||||||
|
|
||||||
enc_details.decrypt(private_key).await
|
enc_details.decrypt(private_key).await
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ impl CandidateService {
|
||||||
) -> Result<CreateCandidateResponse, ServiceError> {
|
) -> Result<CreateCandidateResponse, ServiceError> {
|
||||||
let candidate = Query::find_candidate_by_id(db, id).await?
|
let candidate = Query::find_candidate_by_id(db, id).await?
|
||||||
.ok_or(ServiceError::CandidateNotFound)?;
|
.ok_or(ServiceError::CandidateNotFound)?;
|
||||||
let parents = Query::find_candidate_parents(db, candidate.clone()).await?;
|
let parents = Query::find_candidate_parents(db, &candidate).await?;
|
||||||
|
|
||||||
|
|
||||||
let new_password_plain = crypto::random_8_char_string();
|
let new_password_plain = crypto::random_8_char_string();
|
||||||
|
|
@ -145,6 +145,13 @@ impl CandidateService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_candidate(db: &DbConn, candidate: candidate::Model) -> Result<(), ServiceError> {
|
||||||
|
PortfolioService::delete_candidate_root(candidate.application).await?;
|
||||||
|
|
||||||
|
Mutation::delete_candidate(db, candidate).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(in crate::services) async fn add_candidate_details(
|
pub(in crate::services) async fn add_candidate_details(
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
candidate: candidate::Model,
|
candidate: candidate::Model,
|
||||||
|
|
@ -163,7 +170,7 @@ impl CandidateService {
|
||||||
page: Option<u64>,
|
page: Option<u64>,
|
||||||
) -> Result<Vec<BaseCandidateResponse>, ServiceError> {
|
) -> Result<Vec<BaseCandidateResponse>, ServiceError> {
|
||||||
|
|
||||||
let candidates = Query::list_candidates(
|
let candidates = Query::list_candidates_preview(
|
||||||
db,
|
db,
|
||||||
field_of_study,
|
field_of_study,
|
||||||
page
|
page
|
||||||
|
|
@ -374,7 +381,7 @@ pub mod tests {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub async fn put_user_data(db: &DbConn) -> (candidate::Model, Vec<parent::Model>) {
|
pub async fn put_user_data(db: &DbConn) -> (candidate::Model, Vec<parent::Model>) {
|
||||||
use crate::{models::candidate_details::tests::APPLICATION_DETAILS, Query};
|
use crate::models::candidate_details::tests::APPLICATION_DETAILS;
|
||||||
|
|
||||||
let plain_text_password = "test".to_string();
|
let plain_text_password = "test".to_string();
|
||||||
let (candidate, _parent) = ApplicationService::create_candidate_with_parent(
|
let (candidate, _parent) = ApplicationService::create_candidate_with_parent(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use entity::{parent, candidate};
|
use entity::{parent, candidate};
|
||||||
use sea_orm::DbConn;
|
use sea_orm::DbConn;
|
||||||
|
|
||||||
use crate::{error::ServiceError, Mutation, models::{candidate_details::{EncryptedParentDetails}, candidate::ParentDetails}, Query, utils::db::get_recipients};
|
use crate::{error::ServiceError, Mutation, models::{candidate_details::{EncryptedParentDetails}, candidate::ParentDetails}, Query};
|
||||||
|
|
||||||
pub struct ParentService;
|
pub struct ParentService;
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ impl ParentService {
|
||||||
parents_details: &Vec<ParentDetails>,
|
parents_details: &Vec<ParentDetails>,
|
||||||
recipients: &Vec<String>,
|
recipients: &Vec<String>,
|
||||||
) -> Result<Vec<parent::Model>, ServiceError> {
|
) -> Result<Vec<parent::Model>, ServiceError> {
|
||||||
let found_parents = Query::find_candidate_parents(db, ref_candidate.clone()).await?;
|
let found_parents = Query::find_candidate_parents(db, &ref_candidate).await?;
|
||||||
if found_parents.len() > 2 {
|
if found_parents.len() > 2 {
|
||||||
return Err(ServiceError::ParentOverflow);
|
return Err(ServiceError::ParentOverflow);
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ mod tests {
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use crate::{utils::db::get_memory_sqlite_connection, models::{candidate::{ParentDetails, ApplicationDetails, CandidateDetails}, candidate_details::{tests::APPLICATION_DETAILS, EncryptedParentDetails, EncryptedApplicationDetails}}, services::{candidate_service::CandidateService, application_service::ApplicationService}, crypto};
|
use crate::{utils::db::get_memory_sqlite_connection, models::{candidate::{ParentDetails, ApplicationDetails, CandidateDetails}, candidate_details::EncryptedApplicationDetails}, services::{candidate_service::CandidateService, application_service::ApplicationService}, crypto};
|
||||||
|
|
||||||
use super::ParentService;
|
use super::ParentService;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{path::{PathBuf, Path}};
|
use std::{path::{PathBuf, Path}};
|
||||||
|
|
||||||
use entity::candidate;
|
use entity::candidate;
|
||||||
use log::info;
|
use log::{info, warn};
|
||||||
use sea_orm::{DbConn};
|
use sea_orm::{DbConn};
|
||||||
use serde::{Serialize, ser::{SerializeStruct}};
|
use serde::{Serialize, ser::{SerializeStruct}};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
@ -351,6 +351,18 @@ impl PortfolioService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes all candidate folder. Used ONLY when candidate is deleted!
|
||||||
|
pub async fn delete_candidate_root(candidate_id: i32) -> Result<(), ServiceError> {
|
||||||
|
warn!("CANDIDATE {} ROOT DIRECTORY DELETE STARTED", candidate_id);
|
||||||
|
|
||||||
|
let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf();
|
||||||
|
tokio::fs::remove_dir_all(path).await?;
|
||||||
|
|
||||||
|
warn!("CANDIDATE {} ROOT DIRECTORY DELETE FINISHED", candidate_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if portfolio is submitted
|
/// Returns true if portfolio is submitted
|
||||||
pub async fn is_portfolio_submitted(candidate_id: i32) -> bool {
|
pub async fn is_portfolio_submitted(candidate_id: i32) -> bool {
|
||||||
let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf();
|
let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
use sea_orm::{DbConn};
|
use sea_orm::{DbConn};
|
||||||
use crate::{error::ServiceError, models::candidate_details::{EncryptedApplicationDetails}, Query, models::candidate::{CandidateWithParent, ApplicationDetails}};
|
use crate::{error::ServiceError, models::candidate_details::{EncryptedApplicationDetails}, Query, models::candidate::{Row, ApplicationDetails}};
|
||||||
|
|
||||||
|
|
||||||
type Row = CandidateWithParent;
|
|
||||||
|
|
||||||
impl From<(i32, ApplicationDetails)> for Row {
|
impl From<(i32, ApplicationDetails)> for Row {
|
||||||
fn from((application, d): (i32, ApplicationDetails)) -> Self {
|
fn from((application, d): (i32, ApplicationDetails)) -> Self {
|
||||||
let c = d.candidate;
|
let c = d.candidate;
|
||||||
let p = d.parents[0].clone();
|
|
||||||
Self {
|
Self {
|
||||||
application,
|
application,
|
||||||
name: Some(c.name),
|
name: Some(c.name),
|
||||||
|
|
@ -22,10 +18,15 @@ impl From<(i32, ApplicationDetails)> for Row {
|
||||||
study: Some(c.study),
|
study: Some(c.study),
|
||||||
personal_identification_number: Some(c.personal_id_number),
|
personal_identification_number: Some(c.personal_id_number),
|
||||||
|
|
||||||
parent_name: Some(p.name),
|
parent_name: d.parents.get(0).map(|p| p.name.clone()),
|
||||||
parent_surname: Some(p.surname),
|
parent_surname: d.parents.get(0).map(|p| p.surname.clone()),
|
||||||
parent_telephone: Some(p.telephone),
|
parent_telephone: d.parents.get(0).map(|p| p.telephone.clone()),
|
||||||
parent_email: Some(p.email),
|
parent_email: d.parents.get(0).map(|p| p.email.clone()),
|
||||||
|
|
||||||
|
second_parent_name: d.parents.get(1).map(|p| p.name.clone()),
|
||||||
|
second_parent_surname: d.parents.get(1).map(|p| p.surname.clone()),
|
||||||
|
second_parent_telephone: d.parents.get(1).map(|p| p.telephone.clone()),
|
||||||
|
second_parent_email: d.parents.get(1).map(|p| p.email.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,11 +37,12 @@ pub async fn export(
|
||||||
) -> Result<Vec<u8>, ServiceError> {
|
) -> Result<Vec<u8>, ServiceError> {
|
||||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||||
|
|
||||||
let candidates_with_parents = Query::list_all_candidates_with_parents(&db).await?;
|
let candidates_with_parents = Query::list_candidates_full(&db).await?;
|
||||||
for candidate in candidates_with_parents {
|
for candidate in candidates_with_parents {
|
||||||
let application = candidate.application;
|
let application = candidate.application;
|
||||||
|
let parents = Query::find_candidate_parents(db, &candidate).await?;
|
||||||
|
|
||||||
let row: Row = match EncryptedApplicationDetails::try_from(candidate) {
|
let row: Row = match EncryptedApplicationDetails::try_from((candidate, parents)) {
|
||||||
Ok(d) => Row::from(
|
Ok(d) => Row::from(
|
||||||
d
|
d
|
||||||
.decrypt(private_key.to_string())
|
.decrypt(private_key.to_string())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue