mirror of
https://github.com/danbulant/Portfolio
synced 2026-06-16 13:01:13 +00:00
Merge pull request #150 from EETagent/link_two_applications
Refactoring - link two applications with personal id number
This commit is contained in:
commit
b44ce9c559
61 changed files with 1505 additions and 944 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -16,4 +16,5 @@ target/
|
|||
|
||||
.env
|
||||
|
||||
Rocket.toml
|
||||
Rocket.toml
|
||||
output.log
|
||||
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -2155,14 +2155,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "portfolio"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"portfolio-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portfolio-api"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
|
|
@ -2193,7 +2193,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "portfolio-core"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"aes-gcm-siv",
|
||||
"age",
|
||||
|
|
@ -2223,7 +2223,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "portfolio-entity"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
|
|
@ -2232,7 +2232,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "portfolio-migration"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"portfolio-entity",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "portfolio"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
authors = ["Vojtěch Jungmann", "Sebastian Pravda"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "portfolio-api"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use entity::candidate::Model as Candidate;
|
||||
use entity::application::Model as Application;
|
||||
use portfolio_core::models::auth::AuthenticableTrait;
|
||||
use portfolio_core::sea_orm::prelude::Uuid;
|
||||
use portfolio_core::services::candidate_service::CandidateService;
|
||||
use portfolio_core::services::application_service::ApplicationService;
|
||||
use rocket::http::Status;
|
||||
use rocket::outcome::Outcome;
|
||||
use rocket::request::{FromRequest, Request};
|
||||
|
|
@ -9,26 +9,26 @@ use rocket::request::{FromRequest, Request};
|
|||
use crate::logging::format_request;
|
||||
use crate::pool::Db;
|
||||
|
||||
pub struct CandidateAuth(Candidate, String);
|
||||
pub struct ApplicationAuth(Application, String);
|
||||
|
||||
impl Into<Candidate> for CandidateAuth {
|
||||
fn into(self) -> Candidate {
|
||||
impl Into<Application> for ApplicationAuth {
|
||||
fn into(self) -> Application {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl CandidateAuth {
|
||||
impl ApplicationAuth {
|
||||
pub fn get_private_key(&self) -> String {
|
||||
self.1.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for CandidateAuth {
|
||||
impl<'r> FromRequest<'r> for ApplicationAuth {
|
||||
type Error = Option<String>;
|
||||
async fn from_request(
|
||||
req: &'r Request<'_>,
|
||||
) -> Outcome<CandidateAuth, (Status, Self::Error), ()> {
|
||||
) -> Outcome<ApplicationAuth, (Status, Self::Error), ()> {
|
||||
let cookie_id = req.cookies().get_private("id");
|
||||
let cookie_private_key = req.cookies().get_private("key");
|
||||
|
||||
|
|
@ -50,12 +50,12 @@ impl<'r> FromRequest<'r> for CandidateAuth {
|
|||
Err(_) => return Outcome::Failure((Status::BadRequest, None)),
|
||||
};
|
||||
|
||||
let session = CandidateService::auth(conn, uuid).await;
|
||||
let session = ApplicationService::auth(conn, uuid).await;
|
||||
|
||||
match session {
|
||||
Ok(model) => {
|
||||
info!("{}: CANDIDATE {} AUTHENTICATED", format_request(req), model.application);
|
||||
Outcome::Success(CandidateAuth(model, private_key.to_string().to_string()))
|
||||
info!("{}: CANDIDATE {} AUTHENTICATED", format_request(req), model.id);
|
||||
Outcome::Success(ApplicationAuth(model, private_key.to_string().to_string()))
|
||||
},
|
||||
Err(e) => {
|
||||
info!("{}: CANDIDATE {} AUTHENTICATION FAILED", format_request(req), e);
|
||||
|
|
|
|||
|
|
@ -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}, auth::AuthenticableTrait}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv,
|
||||
services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::{candidate::{CreateCandidateResponse, ApplicationDetails}, auth::AuthenticableTrait, application::ApplicationResponse}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv,
|
||||
};
|
||||
use requests::{AdminLoginRequest, RegisterRequest};
|
||||
use rocket::http::{Cookie, Status, CookieJar};
|
||||
|
|
@ -85,20 +85,16 @@ pub async fn hello(_session: AdminAuth) -> Result<String, Custom<String>> {
|
|||
#[post("/create", data = "<request>")]
|
||||
pub async fn create_candidate(
|
||||
conn: Connection<'_, Db>,
|
||||
_session: AdminAuth,
|
||||
session: AdminAuth,
|
||||
request: Json<RegisterRequest>,
|
||||
) -> Result<Json<CreateCandidateResponse>, Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
let form = request.into_inner();
|
||||
let private_key = session.get_private_key();
|
||||
|
||||
let plain_text_password = random_12_char_string();
|
||||
|
||||
ApplicationService::create_candidate_with_parent(
|
||||
db,
|
||||
form.application_id,
|
||||
&plain_text_password,
|
||||
form.personal_id_number.clone(),
|
||||
)
|
||||
ApplicationService::create(&private_key, &db, form.application_id, &plain_text_password, form.personal_id_number.clone())
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -113,25 +109,24 @@ pub async fn create_candidate(
|
|||
)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[get("/candidates?<field>&<page>")]
|
||||
pub async fn list_candidates(
|
||||
conn: Connection<'_, Db>,
|
||||
session: AdminAuth,
|
||||
field: Option<String>,
|
||||
page: Option<u64>,
|
||||
) -> Result<Json<Vec<BaseCandidateResponse>>, Custom<String>> {
|
||||
page: Option<u64>,
|
||||
) -> Result<Json<Vec<ApplicationResponse>>, Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
let private_key = session.get_private_key();
|
||||
if let Some(field) = field.clone() {
|
||||
if !(field == "KB".to_string() || field == "IT".to_string() || field == "G") {
|
||||
return Err(Custom(Status::BadRequest, "Invalid field of study".to_string()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let candidates = CandidateService::list_candidates(&private_key, db, field, page)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
let candidates = ApplicationService::list_applications(&private_key, db, field, page)
|
||||
.await.map_err(to_custom_error)?;
|
||||
|
||||
Ok(
|
||||
Json(candidates)
|
||||
|
|
@ -164,7 +159,7 @@ pub async fn get_candidate(
|
|||
let db = conn.into_inner();
|
||||
let private_key = session.get_private_key();
|
||||
|
||||
let candidate = Query::find_candidate_by_id(db, id)
|
||||
let application = Query::find_application_by_id(db, id)
|
||||
.await
|
||||
.map_err(|e| to_custom_error(ServiceError::DbError(e)))?
|
||||
.ok_or(to_custom_error(ServiceError::CandidateNotFound))?;
|
||||
|
|
@ -172,7 +167,7 @@ pub async fn get_candidate(
|
|||
let details = ApplicationService::decrypt_all_details(
|
||||
private_key,
|
||||
db,
|
||||
candidate
|
||||
&application
|
||||
)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
|
@ -190,14 +185,22 @@ pub async fn delete_candidate(
|
|||
) -> Result<(), Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
|
||||
let candidate = Query::find_candidate_by_id(db, id)
|
||||
let application = Query::find_application_by_id(db, id)
|
||||
.await
|
||||
.map_err(|e| to_custom_error(ServiceError::DbError(e)))?
|
||||
.ok_or(to_custom_error(ServiceError::CandidateNotFound))?;
|
||||
let candidate = ApplicationService::find_related_candidate(db, &application).await.map_err(to_custom_error)?;
|
||||
|
||||
ApplicationService::delete(db, application).await.map_err(to_custom_error)?;
|
||||
|
||||
let remaining_applications = Query::find_applications_by_candidate_id(db, candidate.id).await
|
||||
.map_err(|e| to_custom_error(ServiceError::DbError(e)))?;
|
||||
|
||||
if remaining_applications.is_empty() {
|
||||
CandidateService::delete_candidate(db, candidate).await.map_err(to_custom_error)?;
|
||||
}
|
||||
|
||||
CandidateService::delete_candidate(db, candidate)
|
||||
.await
|
||||
.map_err(to_custom_error)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/candidate/<id>/reset_password")]
|
||||
|
|
@ -206,13 +209,14 @@ pub async fn reset_candidate_password(
|
|||
session: AdminAuth,
|
||||
id: i32,
|
||||
) -> Result<Json<CreateCandidateResponse>, Custom<String>> {
|
||||
// TODO
|
||||
let db = conn.into_inner();
|
||||
let private_key = session.get_private_key();
|
||||
|
||||
let response = CandidateService::reset_password(private_key, db, id)
|
||||
let response = ApplicationService::reset_password(private_key, db, id)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
||||
Ok(
|
||||
Json(response)
|
||||
)
|
||||
|
|
@ -220,12 +224,19 @@ pub async fn reset_candidate_password(
|
|||
|
||||
#[get("/candidate/<id>/portfolio")]
|
||||
pub async fn get_candidate_portfolio(
|
||||
conn: Connection<'_, Db>,
|
||||
session: AdminAuth,
|
||||
id: i32,
|
||||
) -> Result<Vec<u8>, Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
let private_key = session.get_private_key();
|
||||
|
||||
let portfolio = PortfolioService::get_portfolio(id, private_key)
|
||||
let application = Query::find_application_by_id(db, id)
|
||||
.await
|
||||
.map_err(|e| to_custom_error(ServiceError::DbError(e)))?
|
||||
.ok_or(to_custom_error(ServiceError::CandidateNotFound))?;
|
||||
|
||||
let portfolio = PortfolioService::get_portfolio(application.candidate_id, private_key)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
use entity::application;
|
||||
use portfolio_core::Query;
|
||||
use portfolio_core::error::ServiceError;
|
||||
use portfolio_core::models::auth::AuthenticableTrait;
|
||||
use portfolio_core::models::candidate::{ApplicationDetails, NewCandidateResponse};
|
||||
use portfolio_core::sea_orm::prelude::Uuid;
|
||||
use portfolio_core::services::application_service::ApplicationService;
|
||||
use portfolio_core::services::candidate_service::CandidateService;
|
||||
use portfolio_core::services::portfolio_service::{PortfolioService, SubmissionProgress};
|
||||
use requests::LoginRequest;
|
||||
use rocket::http::{Cookie, CookieJar, Status};
|
||||
|
|
@ -16,7 +17,7 @@ use sea_orm_rocket::Connection;
|
|||
|
||||
use crate::guards::data::letter::Letter;
|
||||
use crate::guards::data::portfolio::Portfolio;
|
||||
use crate::{guards::request::auth::CandidateAuth, pool::Db, requests};
|
||||
use crate::{guards::request::auth::ApplicationAuth, pool::Db, requests};
|
||||
|
||||
use super::to_custom_error;
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ pub async fn login(
|
|||
) -> Result<(), Custom<String>> {
|
||||
let ip_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
|
||||
let db = conn.into_inner();
|
||||
let (session_token, private_key) = CandidateService::login(
|
||||
let (session_token, private_key) = ApplicationService::login(
|
||||
db,
|
||||
login_form.application_id,
|
||||
login_form.password.to_string(),
|
||||
|
|
@ -47,7 +48,7 @@ pub async fn login(
|
|||
#[post("/logout")]
|
||||
pub async fn logout(
|
||||
conn: Connection<'_, Db>,
|
||||
_session: CandidateAuth,
|
||||
_session: ApplicationAuth,
|
||||
cookies: &CookieJar<'_>,
|
||||
) -> Result<(), Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
|
|
@ -61,7 +62,7 @@ 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()))?;
|
||||
let session = Query::find_session_by_uuid(db, session_id).await.unwrap().unwrap(); // TODO
|
||||
CandidateService::logout(db, session)
|
||||
ApplicationService::logout(db, session)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -72,10 +73,21 @@ pub async fn logout(
|
|||
}
|
||||
|
||||
#[get("/whoami")]
|
||||
pub async fn whoami(session: CandidateAuth) -> Result<Json<NewCandidateResponse>, Custom<String>> {
|
||||
pub async fn whoami(conn: Connection<'_, Db>, session: ApplicationAuth) -> Result<Json<NewCandidateResponse>, Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
|
||||
let private_key = session.get_private_key();
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let response = NewCandidateResponse::from_encrypted(&private_key, candidate).await
|
||||
let application: entity::application::Model = session.into();
|
||||
let candidate = ApplicationService::find_related_candidate(&db, &application)
|
||||
.await.map_err(to_custom_error)?; // TODO more compact
|
||||
let applications = Query::find_applications_by_candidate_id(&db, candidate.id)
|
||||
.await.map_err(|e| to_custom_error(ServiceError::DbError(e)))?;
|
||||
let response = NewCandidateResponse::from_encrypted(
|
||||
application.id,
|
||||
applications,
|
||||
&private_key,
|
||||
candidate
|
||||
).await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
Ok(Json(response))
|
||||
|
|
@ -86,13 +98,14 @@ pub async fn whoami(session: CandidateAuth) -> Result<Json<NewCandidateResponse>
|
|||
pub async fn post_details(
|
||||
conn: Connection<'_, Db>,
|
||||
details: Json<ApplicationDetails>,
|
||||
session: CandidateAuth,
|
||||
session: ApplicationAuth,
|
||||
) -> Result<Json<ApplicationDetails>, Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
let form = details.into_inner();
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: application::Model = session.into();
|
||||
let candidate = ApplicationService::find_related_candidate(&db, &application).await.map_err(to_custom_error)?; // TODO
|
||||
|
||||
let _candidate_parent = ApplicationService::add_all_details(db, candidate, &form)
|
||||
let _candidate_parent = ApplicationService::add_all_details(db, &application, candidate, &form)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -102,13 +115,17 @@ pub async fn post_details(
|
|||
#[get("/details")]
|
||||
pub async fn get_details(
|
||||
conn: Connection<'_, Db>,
|
||||
session: CandidateAuth,
|
||||
session: ApplicationAuth,
|
||||
) -> Result<Json<ApplicationDetails>, Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
let private_key = session.get_private_key();
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
let details = ApplicationService::decrypt_all_details(private_key, db, candidate)
|
||||
let details = ApplicationService::decrypt_all_details(
|
||||
private_key,
|
||||
db,
|
||||
&application
|
||||
)
|
||||
.await
|
||||
.map(|x| Json(x))
|
||||
.map_err(to_custom_error);
|
||||
|
|
@ -117,12 +134,12 @@ pub async fn get_details(
|
|||
}
|
||||
#[post("/cover_letter", data = "<letter>")]
|
||||
pub async fn upload_cover_letter(
|
||||
session: CandidateAuth,
|
||||
session: ApplicationAuth,
|
||||
letter: Letter,
|
||||
) -> Result<(), Custom<String>> {
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
PortfolioService::add_cover_letter_to_cache(candidate.application, letter.into())
|
||||
PortfolioService::add_cover_letter_to_cache(application.candidate_id, letter.into())
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -130,10 +147,10 @@ pub async fn upload_cover_letter(
|
|||
}
|
||||
|
||||
#[delete("/cover_letter")]
|
||||
pub async fn delete_cover_letter(session: CandidateAuth) -> Result<(), Custom<String>> {
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
pub async fn delete_cover_letter(session: ApplicationAuth) -> Result<(), Custom<String>> {
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
PortfolioService::delete_cover_letter_from_cache(candidate.application)
|
||||
PortfolioService::delete_cover_letter_from_cache(application.candidate_id)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -142,12 +159,12 @@ pub async fn delete_cover_letter(session: CandidateAuth) -> Result<(), Custom<St
|
|||
|
||||
#[post("/portfolio_letter", data = "<letter>")]
|
||||
pub async fn upload_portfolio_letter(
|
||||
session: CandidateAuth,
|
||||
session: ApplicationAuth,
|
||||
letter: Letter,
|
||||
) -> Result<(), Custom<String>> {
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
PortfolioService::add_portfolio_letter_to_cache(candidate.application, letter.into())
|
||||
PortfolioService::add_portfolio_letter_to_cache(application.candidate_id, letter.into())
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -155,10 +172,10 @@ pub async fn upload_portfolio_letter(
|
|||
}
|
||||
|
||||
#[delete("/portfolio_letter")]
|
||||
pub async fn delete_portfolio_letter(session: CandidateAuth) -> Result<(), Custom<String>> {
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
pub async fn delete_portfolio_letter(session: ApplicationAuth) -> Result<(), Custom<String>> {
|
||||
let candidate: entity::application::Model = session.into();
|
||||
|
||||
PortfolioService::delete_portfolio_letter_from_cache(candidate.application)
|
||||
PortfolioService::delete_portfolio_letter_from_cache(candidate.candidate_id)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -167,12 +184,12 @@ pub async fn delete_portfolio_letter(session: CandidateAuth) -> Result<(), Custo
|
|||
|
||||
#[post("/portfolio_zip", data = "<portfolio>")]
|
||||
pub async fn upload_portfolio_zip(
|
||||
session: CandidateAuth,
|
||||
session: ApplicationAuth,
|
||||
portfolio: Portfolio,
|
||||
) -> Result<(), Custom<String>> {
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
PortfolioService::add_portfolio_zip_to_cache(candidate.application, portfolio.into())
|
||||
PortfolioService::add_portfolio_zip_to_cache(application.candidate_id, portfolio.into())
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -180,10 +197,10 @@ pub async fn upload_portfolio_zip(
|
|||
}
|
||||
|
||||
#[delete("/portfolio_zip")]
|
||||
pub async fn delete_portfolio_zip(session: CandidateAuth) -> Result<(), Custom<String>> {
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
pub async fn delete_portfolio_zip(session: ApplicationAuth) -> Result<(), Custom<String>> {
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
PortfolioService::delete_portfolio_zip_from_cache(candidate.application)
|
||||
PortfolioService::delete_portfolio_zip_from_cache(application.candidate_id)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -192,11 +209,11 @@ pub async fn delete_portfolio_zip(session: CandidateAuth) -> Result<(), Custom<S
|
|||
|
||||
#[get("/submission_progress")]
|
||||
pub async fn submission_progress(
|
||||
session: CandidateAuth,
|
||||
session: ApplicationAuth,
|
||||
) -> Result<Json<SubmissionProgress>, Custom<String>> {
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
let progress = PortfolioService::get_submission_progress(candidate.application)
|
||||
let progress = PortfolioService::get_submission_progress(application.candidate_id)
|
||||
.await
|
||||
.map(|x| Json(x))
|
||||
.map_err(to_custom_error);
|
||||
|
|
@ -207,11 +224,12 @@ pub async fn submission_progress(
|
|||
#[post("/submit")]
|
||||
pub async fn submit_portfolio(
|
||||
conn: Connection<'_, Db>,
|
||||
session: CandidateAuth,
|
||||
session: ApplicationAuth,
|
||||
) -> Result<(), Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: entity::application::Model = session.into();
|
||||
let candidate = ApplicationService::find_related_candidate(&db, &application).await.map_err(to_custom_error)?; // TODO
|
||||
|
||||
let submit = PortfolioService::submit(&candidate, &db).await;
|
||||
|
||||
|
|
@ -220,7 +238,7 @@ pub async fn submit_portfolio(
|
|||
// Delete on critical error
|
||||
if e.code() == 500 {
|
||||
// Cleanup
|
||||
PortfolioService::delete_portfolio(candidate.application)
|
||||
PortfolioService::delete_portfolio(application.id)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
@ -232,11 +250,11 @@ pub async fn submit_portfolio(
|
|||
|
||||
#[post("/delete")]
|
||||
pub async fn delete_portfolio(
|
||||
session: CandidateAuth,
|
||||
session: ApplicationAuth,
|
||||
) -> Result<(), Custom<String>> {
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
PortfolioService::delete_portfolio(candidate.application)
|
||||
PortfolioService::delete_portfolio(application.candidate_id)
|
||||
.await
|
||||
.map_err(to_custom_error)?;
|
||||
|
||||
|
|
@ -244,11 +262,11 @@ pub async fn delete_portfolio(
|
|||
}
|
||||
|
||||
#[get("/download")]
|
||||
pub async fn download_portfolio(session: CandidateAuth) -> Result<Vec<u8>, Custom<String>> {
|
||||
pub async fn download_portfolio(session: ApplicationAuth) -> Result<Vec<u8>, Custom<String>> {
|
||||
let private_key = session.get_private_key();
|
||||
let candidate: entity::candidate::Model = session.into();
|
||||
let application: entity::application::Model = session.into();
|
||||
|
||||
let file = PortfolioService::get_portfolio(candidate.application, private_key)
|
||||
let file = PortfolioService::get_portfolio(application.candidate_id, private_key)
|
||||
.await
|
||||
.map_err(to_custom_error);
|
||||
|
||||
|
|
@ -299,8 +317,7 @@ mod tests {
|
|||
\"sex\": \"MALE\",
|
||||
\"personalIdNumber\": \"0101010000\",
|
||||
\"schoolName\": \"29988383\",
|
||||
\"healthInsurance\": \"000\",
|
||||
\"study\": \"KB\"
|
||||
\"healthInsurance\": \"000\"
|
||||
},
|
||||
\"parents\": [
|
||||
{
|
||||
|
|
@ -331,7 +348,7 @@ mod tests {
|
|||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let candidate = response.into_json::<NewCandidateResponse>().unwrap();
|
||||
assert_eq!(candidate.application_id, APPLICATION_ID);
|
||||
// assert_eq!(candidate.id, APPLICATION_ID); // TODO
|
||||
assert_eq!(candidate.personal_id_number, PERSONAL_ID_NUMBER);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,14 +42,13 @@ pub mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
ApplicationService::create_candidate_with_parent(
|
||||
ApplicationService::create(
|
||||
&"".to_string(),
|
||||
db,
|
||||
APPLICATION_ID,
|
||||
&CANDIDATE_PASSWORD.to_string(),
|
||||
PERSONAL_ID_NUMBER.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
PERSONAL_ID_NUMBER.to_string())
|
||||
.await.unwrap();
|
||||
}
|
||||
|
||||
pub fn test_client() -> &'static Mutex<Client> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "portfolio-core"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ use argon2::{
|
|||
Argon2, PasswordHasher as ArgonPasswordHasher, PasswordVerifier as ArgonPasswordVerifier,
|
||||
};
|
||||
use async_compat::CompatExt;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as base64;
|
||||
use futures::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use rand::Rng;
|
||||
use secrecy::ExposeSecret;
|
||||
|
|
@ -123,14 +125,14 @@ pub async fn encrypt_password(
|
|||
})
|
||||
.await??;
|
||||
|
||||
Ok(base64::encode(hash))
|
||||
Ok(base64.encode(hash))
|
||||
}
|
||||
|
||||
pub async fn decrypt_password(
|
||||
password_cipher_text: String,
|
||||
key: String,
|
||||
) -> Result<String, ServiceError> {
|
||||
let input = base64::decode(password_cipher_text)?;
|
||||
let input = base64.decode(password_cipher_text)?;
|
||||
let plain = tokio::task::spawn_blocking(move || {
|
||||
let aes_key_nonce = convert_key_aes256(&key);
|
||||
|
||||
|
|
@ -164,7 +166,7 @@ pub async fn encrypt_password_age(
|
|||
|
||||
encrypt_writer.close().await?;
|
||||
|
||||
Ok(base64::encode(encrypt_buffer))
|
||||
Ok(base64.encode(encrypt_buffer))
|
||||
}
|
||||
|
||||
#[deprecated(note = "Too slow, use AES instead")]
|
||||
|
|
@ -172,7 +174,7 @@ pub async fn decrypt_password_age(
|
|||
password_encrypted: &str,
|
||||
key: &str,
|
||||
) -> Result<String, ServiceError> {
|
||||
let encrypted = base64::decode(password_encrypted)?;
|
||||
let encrypted = base64.decode(password_encrypted)?;
|
||||
|
||||
let decryptor = match age::Decryptor::new_async(&encrypted[..]).await? {
|
||||
age::Decryptor::Passphrase(d) => d,
|
||||
|
|
@ -263,14 +265,14 @@ pub async fn encrypt_password_with_recipients(
|
|||
)
|
||||
.await?;
|
||||
|
||||
Ok(base64::encode(encrypt_buffer))
|
||||
Ok(base64.encode(encrypt_buffer))
|
||||
}
|
||||
|
||||
pub async fn decrypt_password_with_private_key(
|
||||
password_encrypted: &str,
|
||||
key: &str,
|
||||
) -> Result<String, ServiceError> {
|
||||
let encrypted = base64::decode(password_encrypted)?;
|
||||
let encrypted = base64.decode(password_encrypted)?;
|
||||
|
||||
let mut decrypt_buffer = Vec::new();
|
||||
|
||||
|
|
@ -338,6 +340,9 @@ pub async fn decrypt_file_with_private_key_as_buffer<P: AsRef<Path>>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as base64;
|
||||
|
||||
#[test]
|
||||
fn test_random_12_char_string() {
|
||||
for _ in 0..1000 {
|
||||
|
|
@ -406,7 +411,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(base64::decode(encrypted).is_ok());
|
||||
assert!(base64.decode(encrypted).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -433,7 +438,7 @@ mod tests {
|
|||
#[allow(deprecated)]
|
||||
let encrypted = super::encrypt_password_age(PASSWORD, KEY).await.unwrap();
|
||||
|
||||
assert!(base64::decode(encrypted).is_ok());
|
||||
assert!(base64.decode(encrypted).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -466,7 +471,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(base64::decode(encrypted).is_ok());
|
||||
assert!(base64.decode(encrypted).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -480,7 +485,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(base64::decode(encrypted).is_ok());
|
||||
assert!(base64.decode(encrypted).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ impl Mutation {
|
|||
) -> Result<admin_session::Model, DbErr> {
|
||||
admin_session::ActiveModel {
|
||||
id: Set(random_uuid),
|
||||
admin_id: Set(Some(admin_id)),
|
||||
admin_id: Set(admin_id),
|
||||
ip_address: Set(ip_addr),
|
||||
created_at: Set(Utc::now().naive_local()),
|
||||
expires_at: Set(Utc::now()
|
||||
|
|
|
|||
65
core/src/database/mutation/application.rs
Normal file
65
core/src/database/mutation/application.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use ::entity::application;
|
||||
use log::{info, warn};
|
||||
use sea_orm::{DbConn, DbErr, Set, ActiveModelTrait, IntoActiveModel, DeleteResult, ModelTrait};
|
||||
|
||||
use crate::{Mutation, models::candidate::FieldOfStudy};
|
||||
|
||||
impl Mutation {
|
||||
pub async fn create_application(
|
||||
db: &DbConn,
|
||||
application_id: i32,
|
||||
candidate_id: i32,
|
||||
hashed_password: String,
|
||||
enc_personal_id_number: String,
|
||||
pubkey: String,
|
||||
encrypted_priv_key: String,
|
||||
) -> Result<application::Model, DbErr> {
|
||||
let field_of_study = FieldOfStudy::from(application_id);
|
||||
let insert = application::ActiveModel {
|
||||
id: Set(application_id),
|
||||
field_of_study: Set(field_of_study.into()),
|
||||
personal_id_number: Set(enc_personal_id_number),
|
||||
password: Set(hashed_password),
|
||||
candidate_id: Set(candidate_id),
|
||||
public_key: Set(pubkey),
|
||||
private_key: Set(encrypted_priv_key),
|
||||
created_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
}
|
||||
.insert(db)
|
||||
.await?;
|
||||
|
||||
info!("APPLICATION {} CREATED", application_id);
|
||||
Ok(insert)
|
||||
}
|
||||
|
||||
pub async fn delete_application(
|
||||
db: &DbConn,
|
||||
application: application::Model,
|
||||
) -> Result<DeleteResult, DbErr> {
|
||||
let application_id = application.id;
|
||||
let delete = application.delete(db).await?;
|
||||
|
||||
warn!("APPLICATION {} DELETED", application_id);
|
||||
Ok(delete)
|
||||
}
|
||||
|
||||
pub async fn update_application_password_and_keys(
|
||||
db: &DbConn,
|
||||
application: application::Model,
|
||||
new_password_hash: String,
|
||||
pub_key: String,
|
||||
priv_key_enc: String,
|
||||
) -> Result<application::Model, DbErr> {
|
||||
let application_id = application.id;
|
||||
let mut application = application.into_active_model();
|
||||
application.password = Set(new_password_hash);
|
||||
application.public_key = Set(pub_key);
|
||||
application.private_key = Set(priv_key_enc);
|
||||
|
||||
let update = application.update(db).await?;
|
||||
|
||||
warn!("CANDIDATE {} PASSWORD CHANGED", application_id);
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,16 @@
|
|||
use crate::{Mutation, models::candidate_details::{EncryptedCandidateDetails}};
|
||||
|
||||
use ::entity::candidate::{self};
|
||||
use ::entity::candidate;
|
||||
use log::{info, warn};
|
||||
use sea_orm::*;
|
||||
|
||||
impl Mutation {
|
||||
pub async fn create_candidate(
|
||||
db: &DbConn,
|
||||
application_id: i32,
|
||||
hashed_password: String,
|
||||
enc_personal_id_number: String,
|
||||
pubkey: String,
|
||||
encrypted_priv_key: String,
|
||||
) -> Result<candidate::Model, DbErr> {
|
||||
let insert = candidate::ActiveModel {
|
||||
application: Set(application_id),
|
||||
let candidate = candidate::ActiveModel {
|
||||
personal_identification_number: Set(enc_personal_id_number),
|
||||
code: Set(hashed_password),
|
||||
public_key: Set(pubkey),
|
||||
private_key: Set(encrypted_priv_key),
|
||||
created_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
..Default::default()
|
||||
|
|
@ -26,47 +18,29 @@ impl Mutation {
|
|||
.insert(db)
|
||||
.await?;
|
||||
|
||||
info!("CANDIDATE {} CREATED", application_id);
|
||||
Ok(insert)
|
||||
info!("CANDIDATE {} CREATED", candidate.id);
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
pub async fn delete_candidate(
|
||||
db: &DbConn,
|
||||
candidate: candidate::Model,
|
||||
) -> Result<DeleteResult, DbErr> {
|
||||
let application = candidate.application;
|
||||
let application = candidate.id;
|
||||
let delete = candidate.delete(db).await?;
|
||||
|
||||
warn!("CANDIDATE {} DELETED", application);
|
||||
Ok(delete)
|
||||
}
|
||||
|
||||
pub async fn update_candidate_password_and_keys(
|
||||
db: &DbConn,
|
||||
candidate: candidate::Model,
|
||||
new_password_hash: String,
|
||||
pub_key: String,
|
||||
priv_key_enc: String,
|
||||
) -> Result<candidate::Model, DbErr> {
|
||||
let application = candidate.application;
|
||||
let mut candidate: candidate::ActiveModel = candidate.into();
|
||||
candidate.code = Set(new_password_hash);
|
||||
candidate.public_key = Set(pub_key);
|
||||
candidate.private_key = Set(priv_key_enc);
|
||||
|
||||
let update = candidate.update(db).await?;
|
||||
|
||||
warn!("CANDIDATE {} PASSWORD CHANGED", application);
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
pub async fn update_candidate_details(
|
||||
db: &DbConn,
|
||||
user: candidate::Model,
|
||||
candidate: candidate::Model,
|
||||
enc_candidate: EncryptedCandidateDetails,
|
||||
encrypted_by_id: i32,
|
||||
) -> Result<candidate::Model, sea_orm::DbErr> {
|
||||
let application = user.application;
|
||||
let mut candidate: candidate::ActiveModel = user.into();
|
||||
let application = candidate.id;
|
||||
let mut candidate: candidate::ActiveModel = candidate.into();
|
||||
|
||||
candidate.name = Set(enc_candidate.name.map(|e| e.into()));
|
||||
candidate.surname = Set(enc_candidate.surname.map(|e| e.into()));
|
||||
|
|
@ -77,10 +51,10 @@ impl Mutation {
|
|||
candidate.citizenship = Set(enc_candidate.citizenship.map(|e| e.into()));
|
||||
candidate.email = Set(enc_candidate.email.map(|e| e.into()));
|
||||
candidate.sex = Set(enc_candidate.sex.map(|e| e.into()));
|
||||
candidate.personal_identification_number = Set(enc_candidate.personal_id_number.map(|e| e.into()).unwrap_or_default()); // TODO: do not set this here, it is already set in the create_candidate mutation???
|
||||
// candidate.personal_identification_number = Set(enc_candidate.personal_id_number.map(|e| e.into()).unwrap_or_default()); // TODO: do not set this here, it is already set in the create_candidate mutation???
|
||||
candidate.school_name = Set(enc_candidate.school_name.map(|e| e.into()));
|
||||
candidate.health_insurance = Set(enc_candidate.health_insurance.map(|e| e.into()));
|
||||
candidate.study = Set(enc_candidate.study.map(|e| e.into()));
|
||||
candidate.encrypted_by_id = Set(Some(encrypted_by_id));
|
||||
|
||||
candidate.updated_at = Set(chrono::offset::Local::now().naive_local());
|
||||
|
||||
|
|
@ -90,6 +64,20 @@ impl Mutation {
|
|||
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
pub async fn update_personal_id(
|
||||
db: &DbConn,
|
||||
candidate: candidate::Model,
|
||||
personal_id: &str,
|
||||
) -> Result<candidate::Model, DbErr> {
|
||||
let mut candidate = candidate.into_active_model();
|
||||
candidate.personal_identification_number = Set(personal_id.to_string());
|
||||
|
||||
candidate
|
||||
.update(db)
|
||||
.await
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -103,20 +91,14 @@ mod tests {
|
|||
async fn test_create_candidate() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
|
||||
const APPLICATION_ID: i32 = 103158;
|
||||
|
||||
Mutation::create_candidate(
|
||||
let candidate = Mutation::create_candidate(
|
||||
&db,
|
||||
APPLICATION_ID,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let candidate = Query::find_candidate_by_id(&db, APPLICATION_ID)
|
||||
let candidate = Query::find_candidate_by_id(&db, candidate.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(candidate.is_some());
|
||||
|
|
@ -126,15 +108,9 @@ mod tests {
|
|||
async fn test_add_candidate_details() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
|
||||
const APPLICATION_ID: i32 = 103158;
|
||||
|
||||
let candidate = Mutation::create_candidate(
|
||||
&db,
|
||||
APPLICATION_ID,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -144,12 +120,12 @@ mod tests {
|
|||
vec!["age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_string()],
|
||||
).await.unwrap();
|
||||
|
||||
Mutation::update_candidate_details(&db, candidate, encrypted_details.candidate).await.unwrap();
|
||||
let candidate = Mutation::update_candidate_details(&db, candidate, encrypted_details.candidate, 1).await.unwrap();
|
||||
|
||||
let candidate = Query::find_candidate_by_id(&db, APPLICATION_ID)
|
||||
let candidate = Query::find_candidate_by_id(&db, candidate.id)
|
||||
.await
|
||||
.unwrap().unwrap();
|
||||
|
||||
assert!(candidate.study.is_some());
|
||||
assert!(candidate.name.is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub(crate) struct Mutation;
|
||||
|
||||
pub mod application;
|
||||
pub mod session;
|
||||
pub mod candidate;
|
||||
pub mod parent;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use sea_orm::*;
|
|||
impl Mutation {
|
||||
pub async fn create_parent(db: &DbConn, application_id: i32) -> Result<Model, DbErr> {
|
||||
parent::ActiveModel {
|
||||
application: Set(application_id),
|
||||
candidate_id: Set(application_id),
|
||||
created_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
..Default::default()
|
||||
|
|
@ -49,43 +49,31 @@ mod tests {
|
|||
async fn test_create_parent() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
|
||||
const APPLICATION_ID: i32 = 103158;
|
||||
|
||||
Mutation::create_candidate(
|
||||
let candidate = Mutation::create_candidate(
|
||||
&db,
|
||||
APPLICATION_ID,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_parent = Mutation::create_parent(&db, APPLICATION_ID).await.unwrap();
|
||||
Mutation::create_parent(&db, candidate.id).await.unwrap();
|
||||
|
||||
let parent = Query::find_parent_by_id(&db, new_parent.id).await.unwrap();
|
||||
assert!(parent.is_some());
|
||||
let parents = Query::find_candidate_parents(&db, &candidate).await.unwrap();
|
||||
assert!(parents.get(0).is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_candidate_details() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
|
||||
const APPLICATION_ID: i32 = 103158;
|
||||
|
||||
Mutation::create_candidate(
|
||||
let candidate = Mutation::create_candidate(
|
||||
&db,
|
||||
APPLICATION_ID,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let parent = Mutation::create_parent(&db, APPLICATION_ID).await.unwrap();
|
||||
let parent = Mutation::create_parent(&db, candidate.id).await.unwrap();
|
||||
|
||||
let encrypted_details: EncryptedApplicationDetails = EncryptedApplicationDetails::new(
|
||||
&APPLICATION_DETAILS.lock().unwrap().clone(),
|
||||
|
|
@ -94,15 +82,14 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let parent = Mutation::add_parent_details(&db, parent, encrypted_details.parents[0].clone())
|
||||
Mutation::add_parent_details(&db, parent, encrypted_details.parents[0].clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let parent = Query::find_parent_by_id(&db, parent.id)
|
||||
let parents = Query::find_candidate_parents(&db, &candidate)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(parent.surname.is_some());
|
||||
assert!(parents[0].surname.is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ impl Mutation {
|
|||
) -> Result<session::Model, DbErr> {
|
||||
session::ActiveModel {
|
||||
id: Set(random_uuid),
|
||||
candidate_id: Set(Some(candidate_id)),
|
||||
candidate_id: Set(candidate_id),
|
||||
ip_address: Set(ip_addr),
|
||||
created_at: Set(Utc::now().naive_local()),
|
||||
expires_at: Set(Utc::now()
|
||||
|
|
|
|||
90
core/src/database/query/application.rs
Normal file
90
core/src/database/query/application.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use entity::{application, candidate};
|
||||
use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait};
|
||||
|
||||
const PAGE_SIZE: u64 = 20;
|
||||
|
||||
#[derive(FromQueryResult, Clone)]
|
||||
pub struct ApplicationCandidateJoin {
|
||||
pub application_id: i32,
|
||||
// pub personal_id_number: String,
|
||||
pub candidate_id: i32,
|
||||
pub name: Option<String>,
|
||||
pub surname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub telephone: Option<String>,
|
||||
}
|
||||
|
||||
use crate::{Query};
|
||||
|
||||
impl Query {
|
||||
pub async fn find_application_by_id(
|
||||
db: &DbConn,
|
||||
application_id: i32,
|
||||
) -> Result<Option<application::Model>, DbErr> {
|
||||
application::Entity::find_by_id(application_id)
|
||||
.one(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_related_candidate(
|
||||
db: &DbConn,
|
||||
application: &application::Model,
|
||||
) -> Result<Option<candidate::Model>, DbErr> {
|
||||
application
|
||||
.find_related(candidate::Entity)
|
||||
.one(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_applications(
|
||||
db: &DbConn,
|
||||
field_of_study: Option<String>,
|
||||
page: Option<u64>,
|
||||
) -> Result<Vec<ApplicationCandidateJoin>, DbErr> {
|
||||
let select = application::Entity::find();
|
||||
let query = if let Some(field) = field_of_study {
|
||||
select.filter(application::Column::FieldOfStudy.eq(field))
|
||||
} else {
|
||||
select
|
||||
}
|
||||
.order_by(application::Column::Id, sea_orm::Order::Asc)
|
||||
.join(JoinType::InnerJoin, application::Relation::Candidate.def())
|
||||
.column_as(application::Column::Id, "application_id")
|
||||
.column_as(candidate::Column::Id, "candidate_id")
|
||||
.column_as(candidate::Column::Name, "name")
|
||||
.column_as(candidate::Column::Surname, "surname")
|
||||
.column_as(candidate::Column::Email, "email")
|
||||
.column_as(candidate::Column::Telephone, "telephone")
|
||||
.into_model::<ApplicationCandidateJoin>();
|
||||
|
||||
if let Some(page) = page {
|
||||
query
|
||||
.paginate(db, PAGE_SIZE)
|
||||
.fetch_page(page).await
|
||||
} else {
|
||||
query
|
||||
.all(db).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_applications_compact(
|
||||
db: &DbConn,
|
||||
) -> Result<Vec<application::Model>, DbErr> {
|
||||
application::Entity::find()
|
||||
.join(JoinType::InnerJoin, application::Relation::Candidate.def())
|
||||
.all(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_applications_by_candidate_id(
|
||||
db: &DbConn,
|
||||
candidate_id: i32,
|
||||
) -> Result<Vec<application::Model>, DbErr> {
|
||||
let applications = application::Entity::find()
|
||||
.filter(application::Column::CandidateId.eq(candidate_id))
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
Ok(applications)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,12 @@ use crate::Query;
|
|||
|
||||
pub const PAGE_SIZE: u64 = 20;
|
||||
|
||||
#[derive(FromQueryResult)]
|
||||
pub struct IdPersonalIdNumberJoin {
|
||||
pub id: i32,
|
||||
pub personal_id_number: String,
|
||||
}
|
||||
|
||||
#[derive(FromQueryResult)]
|
||||
pub struct ApplicationId {
|
||||
application: i32,
|
||||
|
|
@ -38,35 +44,11 @@ impl Query {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn list_candidates_preview(
|
||||
db: &DbConn,
|
||||
field_of_study_opt: Option<String>,
|
||||
page: Option<u64>,
|
||||
) -> Result<Vec<CandidateResult>, DbErr> {
|
||||
let select = Candidate::find();
|
||||
let query = if let Some(study) = field_of_study_opt {
|
||||
select.filter(candidate::Column::Study.eq(study))
|
||||
} else {
|
||||
select
|
||||
}
|
||||
.order_by(candidate::Column::Application, Order::Asc)
|
||||
.into_model::<CandidateResult>();
|
||||
|
||||
if let Some(page) = page {
|
||||
query
|
||||
.paginate(db, PAGE_SIZE)
|
||||
.fetch_page(page).await
|
||||
} else {
|
||||
query
|
||||
.all(db).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_candidates_full(
|
||||
db: &DbConn
|
||||
) -> Result<Vec<candidate::Model>, DbErr> {
|
||||
Candidate::find()
|
||||
.order_by(candidate::Column::Application, Order::Asc)
|
||||
.order_by(candidate::Column::Id, Order::Asc)
|
||||
.all(db)
|
||||
.await
|
||||
}
|
||||
|
|
@ -75,13 +57,22 @@ impl Query {
|
|||
db: &DbConn,
|
||||
) -> Result<Vec<ApplicationId>, DbErr> {
|
||||
Candidate::find()
|
||||
.order_by(candidate::Column::Application, Order::Asc)
|
||||
.column(candidate::Column::Application)
|
||||
.order_by(candidate::Column::Id, Order::Asc)
|
||||
.column(candidate::Column::Id)
|
||||
.into_model::<ApplicationId>()
|
||||
.all(db)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
pub async fn find_candidate_by_personal_id(
|
||||
db: &DbConn,
|
||||
personal_id: &str,
|
||||
) -> Result<Option<candidate::Model>, DbErr> {
|
||||
Candidate::find()
|
||||
.filter(candidate::Column::PersonalIdentificationNumber.eq(personal_id))
|
||||
.one(db)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -97,10 +88,7 @@ mod tests {
|
|||
async fn test_find_candidate_by_id() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let candidate = candidate::ActiveModel {
|
||||
application: Set(103158),
|
||||
code: Set("test".to_string()),
|
||||
public_key: Set("test".to_string()),
|
||||
private_key: Set("test".to_string()),
|
||||
id: Set(103158),
|
||||
personal_identification_number: Set("test".to_string()),
|
||||
created_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
|
|
@ -110,7 +98,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let candidate = Query::find_candidate_by_id(&db, candidate.application)
|
||||
let candidate = Query::find_candidate_by_id(&db, candidate.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(candidate.is_some());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub struct Query;
|
||||
|
||||
pub mod application;
|
||||
pub mod candidate;
|
||||
pub mod admin;
|
||||
pub mod session;
|
||||
|
|
|
|||
|
|
@ -2,23 +2,12 @@
|
|||
use entity::candidate;
|
||||
use entity::parent;
|
||||
use entity::parent::Model;
|
||||
use entity::parent::Entity;
|
||||
use sea_orm::ModelTrait;
|
||||
use sea_orm::{DbConn, DbErr};
|
||||
use sea_orm::EntityTrait;
|
||||
|
||||
use crate::Query;
|
||||
|
||||
impl Query {
|
||||
#[deprecated(note = "Use find_candidate_parents instead")]
|
||||
pub async fn find_parent_by_id(
|
||||
db: &DbConn,
|
||||
id: i32,
|
||||
) -> Result<Option<Model>, DbErr> {
|
||||
|
||||
Entity::find_by_id(id).one(db).await
|
||||
}
|
||||
|
||||
pub async fn find_candidate_parents(
|
||||
db: &DbConn,
|
||||
candidate: &candidate::Model,
|
||||
|
|
@ -42,13 +31,10 @@ mod tests {
|
|||
async fn test_find_parent_by_id() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
|
||||
const APPLICATION_ID: i32 = 103158;
|
||||
const CANDIDATE_ID: i32 = 103158;
|
||||
|
||||
candidate::ActiveModel {
|
||||
application: Set(APPLICATION_ID),
|
||||
code: Set("test".to_string()),
|
||||
public_key: Set("test".to_string()),
|
||||
private_key: Set("test".to_string()),
|
||||
id: Set(CANDIDATE_ID),
|
||||
personal_identification_number: Set("test".to_string()),
|
||||
created_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
|
|
@ -58,7 +44,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
let parent = parent::ActiveModel {
|
||||
application: Set(APPLICATION_ID),
|
||||
candidate_id: Set(CANDIDATE_ID),
|
||||
created_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
..Default::default()
|
||||
|
|
@ -67,7 +53,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let parent = Query::find_candidate_by_id(&db, parent.application)
|
||||
let parent = Query::find_candidate_by_id(&db, parent.candidate_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(parent.is_some());
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::Query;
|
||||
|
||||
use ::entity::prelude::AdminSession;
|
||||
use ::entity::{candidate, admin, admin_session};
|
||||
use ::entity::{admin, admin_session, application};
|
||||
use ::entity::{session, session::Entity as Session};
|
||||
use sea_orm::prelude::Uuid;
|
||||
use sea_orm::*;
|
||||
|
|
@ -21,8 +21,8 @@ impl Query {
|
|||
AdminSession::find_by_id(uuid).one(db).await
|
||||
}
|
||||
|
||||
pub async fn find_related_candidate_sessions(db: &DbConn, candidate: &candidate::Model) -> Result<Vec<session::Model>, DbErr> {
|
||||
candidate.find_related(Session)
|
||||
pub async fn find_related_application_sessions(db: &DbConn, application: &application::Model) -> Result<Vec<session::Model>, DbErr> {
|
||||
application.find_related(Session)
|
||||
.order_by_asc(session::Column::UpdatedAt)
|
||||
.all(db)
|
||||
.await
|
||||
|
|
@ -38,23 +38,25 @@ impl Query {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use entity::{session, admin, candidate, admin_session};
|
||||
use entity::{session, admin, admin_session};
|
||||
use sea_orm::{prelude::Uuid, ActiveModelTrait, Set};
|
||||
|
||||
use crate::services::candidate_service::tests::put_user_data;
|
||||
use crate::utils::db::get_memory_sqlite_connection;
|
||||
use crate::Query;
|
||||
use crate::{Query};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_find_session_by_uuid() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
|
||||
let (application, _, _) = put_user_data(&db).await;
|
||||
let session = session::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
candidate_id: Set(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()),
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
..Default::default()
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local())
|
||||
}
|
||||
.insert(&db)
|
||||
.await
|
||||
|
|
@ -70,23 +72,11 @@ mod tests {
|
|||
|
||||
const APPLICATION_ID: i32 = 103158;
|
||||
|
||||
let candidate = candidate::ActiveModel {
|
||||
application: Set(APPLICATION_ID),
|
||||
code: Set("test".to_string()),
|
||||
public_key: Set("test".to_string()),
|
||||
private_key: Set("test".to_string()),
|
||||
personal_identification_number: Set("test".to_string()),
|
||||
created_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
updated_at: Set(chrono::offset::Local::now().naive_local()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
let (application, _, _) = put_user_data(&db).await;
|
||||
|
||||
session::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
candidate_id: Set(Some(APPLICATION_ID)),
|
||||
candidate_id: Set(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()),
|
||||
|
|
@ -115,7 +105,7 @@ mod tests {
|
|||
|
||||
admin_session::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
admin_id: Set(Some(ADMIN_ID)),
|
||||
admin_id: Set(ADMIN_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()),
|
||||
|
|
@ -126,7 +116,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let sessions = Query::find_related_candidate_sessions(&db, &candidate).await.unwrap();
|
||||
let sessions = Query::find_related_application_sessions(&db, &application).await.unwrap();
|
||||
assert_eq!(sessions.len(), 1);
|
||||
|
||||
let sessions = Query::find_related_admin_sessions(&db, &admin).await.unwrap();
|
||||
|
|
|
|||
|
|
@ -18,8 +18,18 @@ pub enum ServiceError {
|
|||
UserAlreadyExists,
|
||||
#[error("Candidate not found")]
|
||||
CandidateNotFound,
|
||||
#[error("Resource is locked")]
|
||||
Locked,
|
||||
#[error("Too many applications")]
|
||||
TooManyApplications,
|
||||
#[error("Too many fields for one person")]
|
||||
TooManyFieldsForOnePerson,
|
||||
#[error("Internal server error")]
|
||||
InternalServerError,
|
||||
#[error("Parrent not found")]
|
||||
ParentNotFound,
|
||||
#[error("Invalid date")]
|
||||
InvalidDate,
|
||||
#[error("Database error")]
|
||||
DbError(#[from] sea_orm::DbErr),
|
||||
#[error("Too many parents")]
|
||||
|
|
@ -69,7 +79,7 @@ pub enum ServiceError {
|
|||
impl ServiceError {
|
||||
pub fn code(&self) -> u16 {
|
||||
match self {
|
||||
// 40X
|
||||
// 4XX
|
||||
ServiceError::InvalidApplicationId => 400,
|
||||
ServiceError::ParentOverflow => 400,
|
||||
ServiceError::Unauthorized => 401,
|
||||
|
|
@ -79,7 +89,12 @@ impl ServiceError {
|
|||
ServiceError::CandidateNotFound => 404,
|
||||
ServiceError::IncompletePortfolio => 406,
|
||||
ServiceError::UserAlreadyExists => 409,
|
||||
ServiceError::Locked => 423,
|
||||
ServiceError::TooManyFieldsForOnePerson => 409,
|
||||
ServiceError::TooManyApplications => 409,
|
||||
// 500
|
||||
ServiceError::InternalServerError => 500,
|
||||
ServiceError::InvalidDate => 500,
|
||||
ServiceError::ParentNotFound => 500,
|
||||
ServiceError::DbError(_) => 500,
|
||||
ServiceError::UserNotFoundBySessionId => 500,
|
||||
|
|
|
|||
69
core/src/models/application.rs
Normal file
69
core/src/models/application.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::{database::query::application::ApplicationCandidateJoin, error::ServiceError};
|
||||
|
||||
use super::candidate_details::EncryptedString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApplicationResponse {
|
||||
pub application_id: i32,
|
||||
// pub personal_id_number: String,
|
||||
pub candidate_id: i32,
|
||||
pub name: String,
|
||||
pub surname: String,
|
||||
pub email: String,
|
||||
pub telephone: String,
|
||||
}
|
||||
|
||||
impl ApplicationResponse {
|
||||
pub async fn from_encrypted(
|
||||
private_key: &String,
|
||||
c: ApplicationCandidateJoin
|
||||
) -> Result<Self, ServiceError> {
|
||||
let name = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.name).ok(), private_key).await?;
|
||||
let surname = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.surname).ok(), private_key).await?;
|
||||
let email = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.email).ok(), private_key).await?;
|
||||
let telephone = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.telephone).ok(), private_key).await?;
|
||||
|
||||
Ok(
|
||||
Self {
|
||||
application_id: c.application_id,
|
||||
name: name.unwrap_or_default(),
|
||||
surname: surname.unwrap_or_default(),
|
||||
email: email.unwrap_or_default(),
|
||||
telephone: telephone.unwrap_or_default(),
|
||||
candidate_id: c.candidate_id,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// CSV export (admin endpoint)
|
||||
#[derive(Serialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApplicationRow {
|
||||
pub application: i32,
|
||||
pub name: Option<String>,
|
||||
pub surname: Option<String>,
|
||||
pub birthplace: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
pub address: Option<String>,
|
||||
pub telephone: Option<String>,
|
||||
pub citizenship: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub sex: Option<String>,
|
||||
pub personal_identification_number: Option<String>,
|
||||
pub school_name: Option<String>,
|
||||
pub health_insurance: Option<String>,
|
||||
|
||||
pub parent_name: Option<String>,
|
||||
pub parent_surname: Option<String>,
|
||||
pub parent_telephone: 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>,
|
||||
}
|
||||
|
|
@ -1,18 +1,50 @@
|
|||
use chrono::NaiveDate;
|
||||
use entity::candidate;
|
||||
use sea_orm::FromQueryResult;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use entity::{application, candidate};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{error::ServiceError, database::query::candidate::CandidateResult, services::portfolio_service::SubmissionProgress};
|
||||
use crate::{
|
||||
error::ServiceError,
|
||||
};
|
||||
|
||||
use super::candidate_details::EncryptedString;
|
||||
use super::candidate_details::{EncryptedString, EncryptedCandidateDetails};
|
||||
|
||||
pub enum FieldOfStudy {
|
||||
G,
|
||||
IT,
|
||||
KB,
|
||||
}
|
||||
|
||||
impl Into<String> for FieldOfStudy {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
FieldOfStudy::G => "G".to_string(),
|
||||
FieldOfStudy::IT => "IT".to_string(),
|
||||
FieldOfStudy::KB => "KB".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for FieldOfStudy {
|
||||
fn from(id: i32) -> Self {
|
||||
match &id.to_string().as_str()[0..3] {
|
||||
"101" => FieldOfStudy::G,
|
||||
"102" => FieldOfStudy::IT,
|
||||
"103" => FieldOfStudy::KB,
|
||||
_ => panic!("Invalid field of study id"), // TODO: handle using TryFrom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal candidate response containing database only not null fields
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NewCandidateResponse {
|
||||
pub application_id: i32,
|
||||
pub current_application: i32,
|
||||
pub applications: Vec<i32>,
|
||||
pub personal_id_number: String,
|
||||
pub details_filled: bool,
|
||||
pub encrypted_by: Option<i32>,
|
||||
pub field_of_study: String,
|
||||
}
|
||||
|
||||
/// Create candidate (admin endpoint)
|
||||
|
|
@ -25,19 +57,6 @@ pub struct CreateCandidateResponse {
|
|||
pub password: String,
|
||||
}
|
||||
|
||||
/// List candidates (admin endpoint)
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BaseCandidateResponse {
|
||||
pub application_id: i32,
|
||||
pub name: String,
|
||||
pub surname: String,
|
||||
pub email: String,
|
||||
pub telephone: String,
|
||||
pub study: String,
|
||||
pub progress: SubmissionProgress,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CandidateDetails {
|
||||
|
|
@ -50,7 +69,6 @@ pub struct CandidateDetails {
|
|||
pub citizenship: String,
|
||||
pub email: String,
|
||||
pub sex: String,
|
||||
pub study: String,
|
||||
pub personal_id_number: String,
|
||||
pub school_name: String,
|
||||
pub health_insurance: String,
|
||||
|
|
@ -73,69 +91,27 @@ pub struct ApplicationDetails {
|
|||
pub parents: Vec<ParentDetails>,
|
||||
}
|
||||
|
||||
/// CSV export (admin endpoint)
|
||||
#[derive(FromQueryResult, Serialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Row {
|
||||
pub application: i32,
|
||||
pub name: Option<String>,
|
||||
pub surname: Option<String>,
|
||||
pub birthplace: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
pub address: Option<String>,
|
||||
pub telephone: Option<String>,
|
||||
pub citizenship: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub sex: Option<String>,
|
||||
pub study: Option<String>,
|
||||
pub personal_identification_number: Option<String>,
|
||||
pub school_name: Option<String>,
|
||||
pub health_insurance: Option<String>,
|
||||
|
||||
pub parent_name: Option<String>,
|
||||
pub parent_surname: Option<String>,
|
||||
pub parent_telephone: 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 NewCandidateResponse {
|
||||
pub async fn from_encrypted(private_key: &String, c: candidate::Model) -> Result<Self, ServiceError> {
|
||||
let id_number = EncryptedString::from(c.personal_identification_number).decrypt(private_key).await?;
|
||||
Ok(
|
||||
Self {
|
||||
application_id: c.application,
|
||||
personal_id_number: id_number,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseCandidateResponse {
|
||||
pub async fn from_encrypted(
|
||||
current_application: i32,
|
||||
applications: Vec<application::Model>,
|
||||
private_key: &String,
|
||||
c: CandidateResult,
|
||||
progress: Option<SubmissionProgress>,
|
||||
c: candidate::Model,
|
||||
) -> Result<Self, ServiceError> {
|
||||
let name = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.name).ok(), private_key).await?;
|
||||
let surname = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.surname).ok(), private_key).await?;
|
||||
let email = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.email).ok(), private_key).await?;
|
||||
let telephone = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.telephone).ok(), private_key).await?;
|
||||
let field_of_study = FieldOfStudy::from(current_application).into();
|
||||
let id_number = EncryptedString::from(c.personal_identification_number.to_owned())
|
||||
.decrypt(private_key)
|
||||
.await?;
|
||||
let applications = applications.iter().map(|a| a.id).collect();
|
||||
let encrypted_details = EncryptedCandidateDetails::from(&c);
|
||||
|
||||
Ok(
|
||||
Self {
|
||||
application_id: c.application,
|
||||
name: name.unwrap_or_default(),
|
||||
surname: surname.unwrap_or_default(),
|
||||
email: email.unwrap_or_default(),
|
||||
telephone: telephone.unwrap_or_default(),
|
||||
study: c.study.unwrap_or_default(),
|
||||
progress: progress.unwrap_or(SubmissionProgress::NoneInCache),
|
||||
}
|
||||
)
|
||||
Ok(Self {
|
||||
current_application,
|
||||
applications,
|
||||
personal_id_number: id_number,
|
||||
details_filled: encrypted_details.is_filled(),
|
||||
encrypted_by: c.encrypted_by_id,
|
||||
field_of_study,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@ use chrono::NaiveDate;
|
|||
use entity::{candidate, parent};
|
||||
use futures::future;
|
||||
|
||||
use crate::{crypto, models::candidate::{Row, ApplicationDetails}, error::ServiceError};
|
||||
use crate::{crypto, models::candidate::{ApplicationDetails}, error::ServiceError, utils::date::parse_naive_date_from_opt_str};
|
||||
|
||||
use super::candidate::{CandidateDetails, ParentDetails};
|
||||
use super::{candidate::{CandidateDetails, ParentDetails}, application::ApplicationRow};
|
||||
|
||||
pub const NAIVE_DATE_FMT: &str = "%Y-%m-%d";
|
||||
|
||||
|
|
@ -26,7 +26,6 @@ pub struct EncryptedCandidateDetails {
|
|||
pub personal_id_number: Option<EncryptedString>,
|
||||
pub school_name: Option<EncryptedString>,
|
||||
pub health_insurance: Option<EncryptedString>,
|
||||
pub study: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -148,7 +147,6 @@ impl EncryptedCandidateDetails {
|
|||
personal_id_number: d.9,
|
||||
school_name: d.10,
|
||||
health_insurance: d.11,
|
||||
study: Some(form.study.clone()),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -173,7 +171,7 @@ impl EncryptedCandidateDetails {
|
|||
name: d.0.unwrap_or_default(),
|
||||
surname: d.1.unwrap_or_default(),
|
||||
birthplace: d.2.unwrap_or_default(),
|
||||
birthdate: NaiveDate::parse_from_str(&d.3.unwrap_or_default(), NAIVE_DATE_FMT).unwrap_or(NaiveDate::from_ymd(1, 1, 1)),
|
||||
birthdate: parse_naive_date_from_opt_str(d.3, NAIVE_DATE_FMT)?,
|
||||
address: d.4.unwrap_or_default(),
|
||||
telephone: d.5.unwrap_or_default(),
|
||||
citizenship: d.6.unwrap_or_default(),
|
||||
|
|
@ -182,7 +180,6 @@ impl EncryptedCandidateDetails {
|
|||
personal_id_number: d.9.unwrap_or_default(),
|
||||
school_name: d.10.unwrap_or_default(),
|
||||
health_insurance: d.11.unwrap_or_default(),
|
||||
study: self.study.clone().unwrap_or_default(),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -196,9 +193,8 @@ impl EncryptedCandidateDetails {
|
|||
self.telephone.is_some() &&
|
||||
self.citizenship.is_some() &&
|
||||
self.email.is_some() &&
|
||||
self.sex.is_some() &&
|
||||
self.personal_id_number.is_some() &&
|
||||
self.study.is_some()
|
||||
// self.sex.is_some() &&
|
||||
self.personal_id_number.is_some()
|
||||
}
|
||||
}
|
||||
impl From<&candidate::Model> for EncryptedCandidateDetails {
|
||||
|
|
@ -218,7 +214,6 @@ impl From<&candidate::Model> for EncryptedCandidateDetails {
|
|||
personal_id_number: Some(EncryptedString::from(candidate.personal_identification_number.to_owned())),
|
||||
school_name: EncryptedString::try_from(&candidate.school_name).ok(),
|
||||
health_insurance: EncryptedString::try_from(&candidate.health_insurance).ok(),
|
||||
study: candidate.study.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -336,11 +331,11 @@ impl From<(&candidate::Model, Vec<parent::Model>)> for EncryptedApplicationDetai
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Row> for EncryptedApplicationDetails {
|
||||
impl TryFrom<ApplicationRow> for EncryptedApplicationDetails {
|
||||
type Error = ServiceError;
|
||||
|
||||
fn try_from(
|
||||
cp: Row,
|
||||
cp: ApplicationRow,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(EncryptedApplicationDetails {
|
||||
candidate: EncryptedCandidateDetails {
|
||||
|
|
@ -356,7 +351,6 @@ impl TryFrom<Row> for EncryptedApplicationDetails {
|
|||
personal_id_number: EncryptedString::try_from(&cp.personal_identification_number).ok(),
|
||||
school_name: EncryptedString::try_from(&cp.school_name).ok(),
|
||||
health_insurance: EncryptedString::try_from(&cp.health_insurance).ok(),
|
||||
study: cp.study.ok_or(ServiceError::CandidateDetailsNotSet).ok(),
|
||||
},
|
||||
parents: vec![EncryptedParentDetails {
|
||||
name: EncryptedString::try_from(&cp.parent_name).ok(),
|
||||
|
|
@ -410,7 +404,6 @@ pub mod tests {
|
|||
personal_id_number: "personal_id_number".to_string(),
|
||||
school_name: "school_name".to_string(),
|
||||
health_insurance: "health_insurance".to_string(),
|
||||
study: "study".to_string(),
|
||||
},
|
||||
parents: vec![ParentDetails {
|
||||
name: "parent_name".to_string(),
|
||||
|
|
@ -431,8 +424,6 @@ pub mod tests {
|
|||
assert_eq!(details.candidate.citizenship, "citizenship");
|
||||
assert_eq!(details.candidate.email, "email");
|
||||
assert_eq!(details.candidate.sex, "sex");
|
||||
assert_eq!(details.candidate.study, "study");
|
||||
assert_eq!(details.candidate.personal_id_number, "personal_id_number");
|
||||
for parent in &details.parents {
|
||||
assert_eq!(parent.name, "parent_name");
|
||||
assert_eq!(parent.surname, "parent_surname");
|
||||
|
|
@ -510,7 +501,7 @@ pub mod tests {
|
|||
let db = get_memory_sqlite_connection().await;
|
||||
let _admin = insert_test_admin(&db).await;
|
||||
|
||||
let (candidate, parents) = put_user_data(&db).await;
|
||||
let (_, candidate, parents) = put_user_data(&db).await;
|
||||
|
||||
let encrypted_details = EncryptedApplicationDetails::try_from((&candidate, parents)).unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod candidate_details;
|
||||
pub mod candidate;
|
||||
pub mod auth;
|
||||
pub mod auth;
|
||||
pub mod application;
|
||||
|
|
@ -56,7 +56,7 @@ impl AuthenticableTrait for AdminService {
|
|||
return Err(ServiceError::ExpiredSession);
|
||||
}
|
||||
|
||||
let admin = Query::find_admin_by_id(db, session.admin_id.unwrap())
|
||||
let admin = Query::find_admin_by_id(db, session.admin_id)
|
||||
.await?
|
||||
.ok_or(ServiceError::CandidateNotFound)?;
|
||||
|
||||
|
|
@ -104,15 +104,35 @@ impl AuthenticableTrait for AdminService {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod admin_tests {
|
||||
use chrono::Local;
|
||||
pub mod admin_tests {
|
||||
use chrono::{Local, Utc};
|
||||
use entity::admin;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
|
||||
|
||||
use crate::{utils::db::get_memory_sqlite_connection, error::ServiceError};
|
||||
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
pub async fn create_admin(db: &DbConn) -> admin::Model {
|
||||
let password = "admin".to_string();
|
||||
let (pubkey, priv_key) = crypto::create_identity();
|
||||
let enc_priv_key = crypto::encrypt_password(priv_key, password).await.unwrap();
|
||||
|
||||
let admin = admin::ActiveModel {
|
||||
name: Set("admin".to_string()),
|
||||
public_key: Set(pubkey),
|
||||
private_key: Set(enc_priv_key),
|
||||
password: Set("admin".to_string()),
|
||||
created_at: Set(Utc::now().naive_utc()),
|
||||
updated_at: Set(Utc::now().naive_utc()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
admin
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_admin_login() -> Result<(), ServiceError> {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,208 @@
|
|||
use entity::{candidate, parent};
|
||||
use sea_orm::DbConn;
|
||||
use async_trait::async_trait;
|
||||
use chrono::Duration;
|
||||
use entity::{candidate, parent, application, session};
|
||||
use sea_orm::{DbConn, prelude::Uuid, IntoActiveModel};
|
||||
|
||||
use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::candidate::ApplicationDetails};
|
||||
use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::EncryptedApplicationDetails, models::{candidate::{ApplicationDetails, CreateCandidateResponse}, candidate_details::EncryptedString, auth::AuthenticableTrait, application::ApplicationResponse}, Mutation, crypto::{hash_password, self}};
|
||||
|
||||
use super::{parent_service::ParentService, candidate_service::CandidateService};
|
||||
use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService};
|
||||
|
||||
const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"];
|
||||
|
||||
pub struct ApplicationService;
|
||||
|
||||
impl ApplicationService {
|
||||
pub async fn create_candidate_with_parent( // uchazeč s maminkou 👩🍼
|
||||
/// Creates a new candidate with:
|
||||
/// Encrypted personal identification number
|
||||
/// Hashed password
|
||||
/// Encrypted private key
|
||||
/// Public key
|
||||
pub async fn create(
|
||||
admin_private_key: &String,
|
||||
db: &DbConn,
|
||||
application_id: i32,
|
||||
plain_text_password: &String,
|
||||
personal_id_number: String,
|
||||
) -> Result<(candidate::Model, parent::Model), ServiceError> {
|
||||
Ok(
|
||||
(
|
||||
CandidateService::create(db, application_id, plain_text_password, personal_id_number).await?,
|
||||
ParentService::create(db, application_id).await?
|
||||
) -> Result<application::Model, ServiceError> {
|
||||
// Check if application id starts with 101, 102 or 103
|
||||
if !Self::is_application_id_valid(application_id) {
|
||||
return Err(ServiceError::InvalidApplicationId);
|
||||
}
|
||||
|
||||
// Check if user with that application id already exists
|
||||
if Query::find_application_by_id(db, application_id)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ServiceError::UserAlreadyExists);
|
||||
}
|
||||
|
||||
let hashed_password = hash_password(plain_text_password.to_string()).await?;
|
||||
let (pubkey, priv_key_plain_text) = crypto::create_identity();
|
||||
let encrypted_priv_key = crypto::encrypt_password(
|
||||
priv_key_plain_text,
|
||||
plain_text_password.to_string()
|
||||
).await?;
|
||||
|
||||
|
||||
let (candidate, enc_personal_id_number) = Self::find_or_create_candidate_with_personal_id(
|
||||
application_id,
|
||||
admin_private_key,
|
||||
db,
|
||||
personal_id_number,
|
||||
&pubkey,
|
||||
).await?;
|
||||
|
||||
|
||||
let application = Mutation::create_application(
|
||||
db,
|
||||
application_id,
|
||||
candidate.id,
|
||||
hashed_password,
|
||||
enc_personal_id_number.to_string(),
|
||||
pubkey,
|
||||
encrypted_priv_key,
|
||||
).await?;
|
||||
|
||||
let applications = Query::find_applications_by_candidate_id(db, candidate.id).await?;
|
||||
if applications.len() >= 3 {
|
||||
for application in applications {
|
||||
ApplicationService::delete(db, application).await?;
|
||||
}
|
||||
return Err(ServiceError::InternalServerError);
|
||||
}
|
||||
|
||||
Ok(application)
|
||||
}
|
||||
|
||||
async fn find_or_create_candidate_with_personal_id(
|
||||
application_id: i32,
|
||||
admin_private_key: &String,
|
||||
db: &DbConn,
|
||||
personal_id_number: String,
|
||||
pubkey: &String,
|
||||
// enc_personal_id_number: &EncryptedString,
|
||||
) -> Result<(candidate::Model, String), ServiceError> {
|
||||
let candidates = Query::list_candidates_full(db).await?;
|
||||
let ids_decrypted = futures::future::join_all(
|
||||
candidates.iter().map(|c| async {(
|
||||
c.id,
|
||||
EncryptedString::from(c.personal_identification_number.clone())
|
||||
.decrypt(admin_private_key)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
)}
|
||||
))
|
||||
.await;
|
||||
|
||||
let found_ids: Vec<&(i32, String)> = ids_decrypted
|
||||
.iter()
|
||||
.filter(|(_, id)| id == &personal_id_number)
|
||||
.collect();
|
||||
|
||||
if let Some((candidate_id, _)) = found_ids.first() {
|
||||
Ok(
|
||||
Self::find_linkable_candidate(db,
|
||||
application_id,
|
||||
*candidate_id,
|
||||
pubkey,
|
||||
personal_id_number
|
||||
).await?
|
||||
)
|
||||
} else {
|
||||
let recipients = get_recipients(db, pubkey).await?;
|
||||
|
||||
let enc_personal_id_number = EncryptedString::new(
|
||||
&personal_id_number,
|
||||
&recipients,
|
||||
).await?;
|
||||
Ok(
|
||||
(
|
||||
CandidateService::create(db, enc_personal_id_number.to_owned().to_string()).await?,
|
||||
enc_personal_id_number.to_string(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_linkable_candidate(
|
||||
db: &DbConn,
|
||||
new_application_id: i32,
|
||||
candidate_id: i32,
|
||||
pubkey: &String,
|
||||
personal_id_number: String,
|
||||
) -> Result<(candidate::Model, String), ServiceError> {
|
||||
let candidate = Query::find_candidate_by_id(db, candidate_id)
|
||||
.await?
|
||||
.ok_or(ServiceError::CandidateNotFound)?;
|
||||
|
||||
let linked_applications = Query::find_applications_by_candidate_id(db, candidate.id).await?;
|
||||
|
||||
if linked_applications.len() > 1 {
|
||||
return Err(ServiceError::TooManyApplications);
|
||||
}
|
||||
|
||||
let linked_application = linked_applications.first().ok_or(ServiceError::CandidateNotFound)?;//TODO
|
||||
|
||||
if linked_application.id.to_string()[0..3] == new_application_id.to_string()[0..3] {
|
||||
return Err(ServiceError::TooManyFieldsForOnePerson);
|
||||
}
|
||||
|
||||
let mut recipients = Query::get_all_admin_public_keys(db).await?;
|
||||
recipients.append(&mut vec![linked_application.public_key.to_owned(), pubkey.to_owned()]);
|
||||
|
||||
|
||||
let enc_personal_id_number = EncryptedString::new(
|
||||
&personal_id_number,
|
||||
&recipients,
|
||||
).await?;
|
||||
|
||||
let candidate = Mutation::update_personal_id(db, candidate, &enc_personal_id_number.to_owned().to_string()).await?;
|
||||
println!("APPLICATIONS {} AND {} ARE LINKED (CANDIDATE {})", new_application_id, linked_application.id, candidate.id);
|
||||
Ok(
|
||||
(candidate, enc_personal_id_number.to_string())
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn delete(db: &DbConn, application: application::Model) -> Result<(), ServiceError> {
|
||||
Mutation::delete_application(db, application).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_application_id_valid(application_id: i32) -> bool {
|
||||
let s = &application_id.to_string();
|
||||
if s.len() <= 3 {
|
||||
// TODO: does the field of study prefix have to be exactly 6 digits? VYRESIT PODLE PRIHLASEK!!!
|
||||
return false;
|
||||
}
|
||||
let field_of_study_prefix = &s[0..3];
|
||||
FIELD_OF_STUDY_PREFIXES.contains(&field_of_study_prefix)
|
||||
}
|
||||
|
||||
pub async fn find_related_candidate(
|
||||
db: &DbConn,
|
||||
application: &application::Model,
|
||||
) -> Result<candidate::Model, ServiceError> {
|
||||
let candidate = Query::find_related_candidate(db, application).await?;
|
||||
if let Some(candidate) = candidate {
|
||||
Ok(candidate)
|
||||
} else {
|
||||
Err(ServiceError::CandidateNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_all_details(
|
||||
db: &DbConn,
|
||||
application: &application::Model,
|
||||
candidate: candidate::Model,
|
||||
form: &ApplicationDetails,
|
||||
) -> Result<(candidate::Model, Vec<parent::Model>), ServiceError> {
|
||||
let mut recipients = Query::get_all_admin_public_keys(db).await?;
|
||||
let applications = Query::find_applications_by_candidate_id(db, candidate.id).await?;
|
||||
recipients.append(&mut applications.iter().map(|a| a.public_key.to_owned()).collect());
|
||||
|
||||
let recipients = get_recipients(db, &candidate.public_key).await?;
|
||||
let candidate = CandidateService::add_candidate_details(db, candidate, &form.candidate, &recipients).await?;
|
||||
|
||||
let candidate = CandidateService::add_candidate_details(db, candidate, &form.candidate, &recipients, application.id).await?;
|
||||
let parents = ParentService::add_parents_details(db, &candidate, &form.parents, &recipients).await?;
|
||||
Ok(
|
||||
(
|
||||
|
|
@ -42,8 +215,10 @@ impl ApplicationService {
|
|||
pub async fn decrypt_all_details(
|
||||
private_key: String,
|
||||
db: &DbConn,
|
||||
candidate: candidate::Model,
|
||||
application: &application::Model,
|
||||
) -> Result<ApplicationDetails, ServiceError> {
|
||||
let candidate = ApplicationService::find_related_candidate(db, application).await?;
|
||||
|
||||
let parents = Query::find_candidate_parents(db, &candidate).await?;
|
||||
let enc_details = EncryptedApplicationDetails::from((&candidate, parents));
|
||||
|
||||
|
|
@ -52,7 +227,268 @@ impl ApplicationService {
|
|||
} else {
|
||||
Err(ServiceError::Forbidden)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
pub async fn list_applications(
|
||||
private_key: &String,
|
||||
db: &DbConn,
|
||||
field_of_study: Option<String>,
|
||||
page: Option<u64>,
|
||||
|
||||
) -> Result<Vec<ApplicationResponse>, ServiceError> {
|
||||
let applications = Query::list_applications(db, field_of_study, page).await?;
|
||||
|
||||
futures::future::try_join_all(
|
||||
applications
|
||||
.iter()
|
||||
.map(|c| async move {
|
||||
ApplicationResponse::from_encrypted(
|
||||
private_key,
|
||||
c.to_owned()
|
||||
).await
|
||||
})
|
||||
).await
|
||||
|
||||
|
||||
}
|
||||
|
||||
async fn decrypt_private_key(
|
||||
application: application::Model,
|
||||
password: String,
|
||||
) -> Result<String, ServiceError> {
|
||||
let private_key_encrypted = application.private_key;
|
||||
|
||||
let private_key = crypto::decrypt_password(private_key_encrypted, password).await?;
|
||||
|
||||
Ok(private_key)
|
||||
}
|
||||
|
||||
pub async fn extend_session_duration_to_14_days(db: &DbConn, session: session::Model) -> Result<session::Model, 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)?;
|
||||
|
||||
Ok(Mutation::update_session_expiration(db, session, new_expires_at).await?)
|
||||
} else {
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub async fn reset_password(
|
||||
admin_private_key: String,
|
||||
db: &DbConn,
|
||||
id: i32,
|
||||
) -> Result<CreateCandidateResponse, ServiceError> {
|
||||
let application = Query::find_application_by_id(db, id).await?
|
||||
.ok_or(ServiceError::CandidateNotFound)?;
|
||||
let candidate = ApplicationService::find_related_candidate(db, &application).await?;
|
||||
let parents = Query::find_candidate_parents(db, &candidate).await?;
|
||||
|
||||
let new_password_plain = crypto::random_12_char_string();
|
||||
let new_password_hash = crypto::hash_password(new_password_plain.clone()).await?;
|
||||
|
||||
let (pubkey, priv_key_plain_text) = crypto::create_identity();
|
||||
let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text.clone(),
|
||||
new_password_plain.to_string()
|
||||
).await?;
|
||||
|
||||
|
||||
Self::delete_old_sessions(db, &application, 0).await?;
|
||||
let application = Mutation::update_application_password_and_keys(db,
|
||||
application,
|
||||
new_password_hash,
|
||||
pubkey.clone(),
|
||||
encrypted_priv_key
|
||||
).await?;
|
||||
|
||||
// user might no have filled his details yet, but personal id number is filled from beginning
|
||||
let personal_id_number = EncryptedString::from(application.personal_id_number.clone())
|
||||
.decrypt(&admin_private_key)
|
||||
.await?;
|
||||
|
||||
let applications = Query::find_applications_by_candidate_id(db, candidate.id).await?;
|
||||
let mut recipients = vec![];
|
||||
let mut admin_public_keys = Query::get_all_admin_public_keys(db).await?;
|
||||
recipients.append(&mut admin_public_keys);
|
||||
recipients.append(&mut applications.iter().map(|a| a.public_key.to_owned()).collect());
|
||||
|
||||
let dec_details = EncryptedApplicationDetails::from((&candidate, parents.clone()))
|
||||
.decrypt(admin_private_key).await?;
|
||||
|
||||
let enc_details = EncryptedApplicationDetails::new(&dec_details, recipients).await?;
|
||||
|
||||
let candidate = Mutation::update_personal_id(db,
|
||||
candidate,
|
||||
&enc_details.candidate.personal_id_number.to_owned()
|
||||
.ok_or(ServiceError::CandidateDetailsNotSet)?.to_string()
|
||||
).await?;
|
||||
|
||||
Mutation::update_candidate_details(db,
|
||||
candidate,
|
||||
enc_details.candidate,
|
||||
application.id
|
||||
).await?;
|
||||
|
||||
for i in 0..enc_details.parents.len() {
|
||||
Mutation::add_parent_details(db, parents[i].clone(), enc_details.parents[i].clone()).await?;
|
||||
}
|
||||
|
||||
Ok(
|
||||
CreateCandidateResponse {
|
||||
application_id: id,
|
||||
personal_id_number,
|
||||
password: new_password_plain,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuthenticableTrait for ApplicationService {
|
||||
type User = application::Model;
|
||||
type Session = session::Model;
|
||||
|
||||
async fn login(
|
||||
db: &DbConn,
|
||||
application_id: i32,
|
||||
password: String,
|
||||
ip_addr: String,
|
||||
) -> Result<(String, String), ServiceError> {
|
||||
let application = Query::find_application_by_id(db, application_id)
|
||||
.await?
|
||||
.ok_or(ServiceError::CandidateNotFound)?;
|
||||
|
||||
let session_id = Self::new_session(db, &application, password.clone(), ip_addr).await?;
|
||||
|
||||
let private_key = Self::decrypt_private_key(application, password).await?;
|
||||
Ok((session_id, private_key))
|
||||
}
|
||||
|
||||
async fn auth(db: &DbConn, session_uuid: Uuid) -> Result<application::Model, ServiceError> {
|
||||
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 application = Query::find_application_by_id(db, session.candidate_id)
|
||||
.await?
|
||||
.ok_or(ServiceError::CandidateNotFound)?;
|
||||
|
||||
Ok(application)
|
||||
}
|
||||
|
||||
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,
|
||||
application: &application::Model,
|
||||
password: String,
|
||||
ip_addr: String,
|
||||
) -> Result<String, ServiceError> {
|
||||
if !crypto::verify_password(password.clone(), application.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_candidate_session(db, random_uuid, application.id, ip_addr).await?;
|
||||
|
||||
Self::delete_old_sessions(db, &application, 3).await?;
|
||||
|
||||
Ok(session.id.to_string())
|
||||
}
|
||||
async fn delete_old_sessions(
|
||||
db: &DbConn,
|
||||
application: &application::Model,
|
||||
keep_n_recent: usize,
|
||||
) -> Result<(), ServiceError> {
|
||||
let sessions = Query::find_related_application_sessions(db, &application)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|s| s.to_owned().into_active_model())
|
||||
.collect();
|
||||
|
||||
SessionService::delete_sessions(db, sessions, keep_n_recent).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod application_tests {
|
||||
use crate::{services::{application_service::ApplicationService, candidate_service::tests::put_user_data}, utils::db::get_memory_sqlite_connection, crypto, models::auth::AuthenticableTrait};
|
||||
use crate::services::admin_service::admin_tests::create_admin;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_application_id_validation() {
|
||||
assert!(ApplicationService::is_application_id_valid(101_101));
|
||||
assert!(ApplicationService::is_application_id_valid(102_107));
|
||||
assert!(ApplicationService::is_application_id_valid(103_109));
|
||||
assert!(!ApplicationService::is_application_id_valid(104_109));
|
||||
assert!(!ApplicationService::is_application_id_valid(100_109));
|
||||
assert!(!ApplicationService::is_application_id_valid(201_109));
|
||||
assert!(!ApplicationService::is_application_id_valid(101));
|
||||
}
|
||||
|
||||
// TODO
|
||||
#[tokio::test]
|
||||
async fn test_password_reset() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let admin = create_admin(&db).await;
|
||||
let (application, _, _) = put_user_data(&db).await;
|
||||
|
||||
let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap();
|
||||
|
||||
assert!(
|
||||
ApplicationService::login(&db, application.id, "test".to_string(), "127.0.0.1".to_string()).await.is_ok()
|
||||
);
|
||||
|
||||
let new_password = ApplicationService::reset_password(private_key, &db, application.id).await.unwrap().password;
|
||||
|
||||
assert!(
|
||||
ApplicationService::login(&db, application.id, "test".to_string(), "127.0.0.1".to_string()).await.is_err()
|
||||
);
|
||||
|
||||
assert!(
|
||||
ApplicationService::login(&db, application.id, new_password, "127.0.0.1".to_string()).await.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_encrypt_decrypt_private_key_with_passphrase() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
|
||||
let plain_text_password = "test".to_string();
|
||||
|
||||
let secret_message = "trnka".to_string();
|
||||
|
||||
let application = ApplicationService::create(&"".to_string(), &db, 103100, &plain_text_password, "".to_string()).await.unwrap();
|
||||
|
||||
let encrypted_message =
|
||||
crypto::encrypt_password_with_recipients(&secret_message, &vec![&application.public_key])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let private_key_plain_text =
|
||||
crypto::decrypt_password(application.private_key, plain_text_password)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let decrypted_message =
|
||||
crypto::decrypt_password_with_private_key(&encrypted_message, &private_key_plain_text)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(secret_message, decrypted_message);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,13 @@
|
|||
use async_trait::async_trait;
|
||||
use chrono::Duration;
|
||||
use entity::{candidate, session};
|
||||
use sea_orm::{prelude::Uuid, DbConn, IntoActiveModel};
|
||||
use entity::candidate;
|
||||
use sea_orm::DbConn;
|
||||
|
||||
use crate::{
|
||||
models::{candidate_details::{EncryptedApplicationDetails, EncryptedString, EncryptedCandidateDetails}, candidate::CandidateDetails},
|
||||
crypto::{self, hash_password},
|
||||
models::{candidate_details::EncryptedCandidateDetails, candidate::CandidateDetails},
|
||||
error::ServiceError,
|
||||
Mutation, Query, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse}, auth::AuthenticableTrait}, utils::db::get_recipients,
|
||||
Mutation,
|
||||
};
|
||||
|
||||
use super::{session_service::SessionService, portfolio_service::PortfolioService};
|
||||
|
||||
|
||||
const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"];
|
||||
use super::{portfolio_service::PortfolioService};
|
||||
|
||||
pub struct CandidateService;
|
||||
|
||||
|
|
@ -25,106 +19,22 @@ impl CandidateService {
|
|||
/// Public key
|
||||
pub(in crate::services) async fn create(
|
||||
db: &DbConn,
|
||||
application_id: i32,
|
||||
plain_text_password: &String,
|
||||
personal_id_number: String,
|
||||
enc_personal_id_number: String,
|
||||
) -> Result<candidate::Model, ServiceError> {
|
||||
// Check if application id starts with 101, 102 or 103
|
||||
if !CandidateService::is_application_id_valid(application_id) {
|
||||
return Err(ServiceError::InvalidApplicationId);
|
||||
}
|
||||
|
||||
// Check if user with that application id already exists
|
||||
if Query::find_candidate_by_id(db, application_id)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ServiceError::UserAlreadyExists);
|
||||
}
|
||||
PortfolioService::create_user_dir(application_id).await?;
|
||||
|
||||
|
||||
let hashed_password = hash_password(plain_text_password.to_string()).await?;
|
||||
let (pubkey, priv_key_plain_text) = crypto::create_identity();
|
||||
let encrypted_priv_key = crypto::encrypt_password(
|
||||
priv_key_plain_text,
|
||||
plain_text_password.to_string()
|
||||
).await?;
|
||||
|
||||
let recipients = get_recipients(db, &pubkey).await?;
|
||||
let enc_personal_id_number = EncryptedString::new(
|
||||
&personal_id_number,
|
||||
&recipients,
|
||||
).await?;
|
||||
|
||||
let candidate = Mutation::create_candidate(
|
||||
db,
|
||||
application_id,
|
||||
hashed_password,
|
||||
enc_personal_id_number.to_string(),
|
||||
pubkey,
|
||||
encrypted_priv_key,
|
||||
enc_personal_id_number,
|
||||
)
|
||||
.await?;
|
||||
|
||||
PortfolioService::create_user_dir(candidate.id).await?;
|
||||
|
||||
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
pub async fn reset_password(
|
||||
admin_private_key: String,
|
||||
db: &DbConn,
|
||||
id: i32,
|
||||
) -> Result<CreateCandidateResponse, ServiceError> {
|
||||
let candidate = Query::find_candidate_by_id(db, id).await?
|
||||
.ok_or(ServiceError::CandidateNotFound)?;
|
||||
let parents = Query::find_candidate_parents(db, &candidate).await?;
|
||||
|
||||
|
||||
let new_password_plain = crypto::random_12_char_string();
|
||||
let new_password_hash = crypto::hash_password(new_password_plain.clone()).await?;
|
||||
|
||||
let (pubkey, priv_key_plain_text) = crypto::create_identity();
|
||||
let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text.clone(),
|
||||
new_password_plain.to_string()
|
||||
).await?;
|
||||
|
||||
|
||||
Self::delete_old_sessions(db, &candidate, 0).await?;
|
||||
let candidate = Mutation::update_candidate_password_and_keys(db,
|
||||
candidate,
|
||||
new_password_hash,
|
||||
pubkey.clone(),
|
||||
encrypted_priv_key
|
||||
).await?;
|
||||
|
||||
|
||||
// user might no have filled his details yet, but personal id number is filled from beginning
|
||||
let personal_id_number = EncryptedString::from(candidate.personal_identification_number.clone())
|
||||
.decrypt(&admin_private_key)
|
||||
.await?;
|
||||
|
||||
let recipients = get_recipients(db, &pubkey).await?;
|
||||
|
||||
let dec_details = EncryptedApplicationDetails::from((&candidate, parents.clone()))
|
||||
.decrypt(admin_private_key).await?;
|
||||
let enc_details = EncryptedApplicationDetails::new(&dec_details, recipients).await?;
|
||||
|
||||
Mutation::update_candidate_details(db, candidate, enc_details.candidate).await?;
|
||||
for i in 0..enc_details.parents.len() {
|
||||
Mutation::add_parent_details(db, parents[i].clone(), enc_details.parents[i].clone()).await?;
|
||||
}
|
||||
|
||||
Ok(
|
||||
CreateCandidateResponse {
|
||||
application_id: id,
|
||||
personal_id_number,
|
||||
password: new_password_plain,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn delete_candidate(db: &DbConn, candidate: candidate::Model) -> Result<(), ServiceError> {
|
||||
PortfolioService::delete_candidate_root(candidate.application).await?;
|
||||
PortfolioService::delete_candidate_root(candidate.id).await?;
|
||||
|
||||
Mutation::delete_candidate(db, candidate).await?;
|
||||
Ok(())
|
||||
|
|
@ -135,297 +45,73 @@ impl CandidateService {
|
|||
candidate: candidate::Model,
|
||||
details: &CandidateDetails,
|
||||
recipients: &Vec<String>,
|
||||
encrypted_by: i32,
|
||||
) -> Result<entity::candidate::Model, ServiceError> {
|
||||
let enc_details = EncryptedCandidateDetails::new(&details, recipients).await?;
|
||||
let model = Mutation::update_candidate_details(db, candidate, enc_details).await?;
|
||||
let model = Mutation::update_candidate_details(
|
||||
db,
|
||||
candidate,
|
||||
enc_details,
|
||||
encrypted_by
|
||||
).await?;
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub async fn list_candidates(
|
||||
private_key: &String,
|
||||
db: &DbConn,
|
||||
field_of_study: Option<String>,
|
||||
page: Option<u64>,
|
||||
) -> Result<Vec<BaseCandidateResponse>, ServiceError> {
|
||||
|
||||
let candidates = Query::list_candidates_preview(
|
||||
db,
|
||||
field_of_study,
|
||||
page
|
||||
).await?;
|
||||
|
||||
futures::future::try_join_all(
|
||||
candidates
|
||||
.iter()
|
||||
.map(|c| async move {
|
||||
BaseCandidateResponse::from_encrypted(
|
||||
private_key,
|
||||
c.clone(),
|
||||
PortfolioService::get_submission_progress(c.application).await.ok()
|
||||
).await
|
||||
})
|
||||
).await
|
||||
}
|
||||
|
||||
async fn decrypt_private_key(
|
||||
candidate: candidate::Model,
|
||||
password: String,
|
||||
) -> Result<String, ServiceError> {
|
||||
let private_key_encrypted = candidate.private_key;
|
||||
|
||||
let private_key = crypto::decrypt_password(private_key_encrypted, password).await?;
|
||||
|
||||
Ok(private_key)
|
||||
}
|
||||
|
||||
fn is_application_id_valid(application_id: i32) -> bool {
|
||||
let s = &application_id.to_string();
|
||||
if s.len() <= 3 {
|
||||
// TODO: does the field of study prefix have to be exactly 6 digits? VYRESIT PODLE PRIHLASEK!!!
|
||||
return false;
|
||||
}
|
||||
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<session::Model, 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)?;
|
||||
|
||||
Ok(Mutation::update_session_expiration(db, session, new_expires_at).await?)
|
||||
} else {
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuthenticableTrait for CandidateService {
|
||||
type User = candidate::Model;
|
||||
type Session = session::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 = Self::new_session(db, &candidate, 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<candidate::Model, ServiceError> {
|
||||
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: 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<String, ServiceError> {
|
||||
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(session.id.to_string())
|
||||
}
|
||||
async fn delete_old_sessions(
|
||||
db: &DbConn,
|
||||
candidate: &candidate::Model,
|
||||
keep_n_recent: usize,
|
||||
) -> Result<(), ServiceError> {
|
||||
let sessions = Query::find_related_candidate_sessions(db, &candidate)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|s| s.to_owned().into_active_model())
|
||||
.collect();
|
||||
|
||||
SessionService::delete_sessions(db, sessions, keep_n_recent).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::services::admin_service::admin_tests::create_admin;
|
||||
use crate::utils::db::get_memory_sqlite_connection;
|
||||
use crate::{crypto, services::candidate_service::CandidateService, Mutation};
|
||||
use crate::{crypto};
|
||||
|
||||
use crate::models::candidate_details::EncryptedApplicationDetails;
|
||||
use entity::{candidate, parent, admin};
|
||||
use entity::{application, candidate, parent};
|
||||
|
||||
use crate::services::application_service::ApplicationService;
|
||||
|
||||
const APPLICATION_ID: i32 = 103151;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_application_id_validation() {
|
||||
assert!(CandidateService::is_application_id_valid(101_101));
|
||||
assert!(CandidateService::is_application_id_valid(102_107));
|
||||
assert!(CandidateService::is_application_id_valid(103_109));
|
||||
assert!(!CandidateService::is_application_id_valid(104_109));
|
||||
assert!(!CandidateService::is_application_id_valid(100_109));
|
||||
assert!(!CandidateService::is_application_id_valid(201_109));
|
||||
assert!(!CandidateService::is_application_id_valid(101));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_password_reset() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let admin = create_admin(&db).await;
|
||||
let (candidate, _parent) = put_user_data(&db).await;
|
||||
|
||||
let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap();
|
||||
|
||||
assert!(
|
||||
CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_ok()
|
||||
);
|
||||
|
||||
let new_password = CandidateService::reset_password(private_key, &db, candidate.application).await.unwrap().password;
|
||||
|
||||
assert!(
|
||||
CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_err()
|
||||
);
|
||||
|
||||
assert!(
|
||||
CandidateService::login(&db, candidate.application, new_password, "127.0.0.1".to_string()).await.is_ok()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_candidates() {
|
||||
async fn test_list_applications() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let admin = create_admin(&db).await;
|
||||
let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap();
|
||||
let candidates = CandidateService::list_candidates(&private_key, &db, None, None).await.unwrap();
|
||||
let candidates = ApplicationService::list_applications(&private_key, &db, None, None).await.unwrap();
|
||||
assert_eq!(candidates.len(), 0);
|
||||
|
||||
put_user_data(&db).await;
|
||||
|
||||
let candidates = CandidateService::list_candidates(&private_key, &db, None, None).await.unwrap();
|
||||
let candidates = ApplicationService::list_applications(&private_key, &db, None, 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;
|
||||
#[cfg(test)]
|
||||
pub async fn put_user_data(db: &DbConn) -> (application::Model, candidate::Model, Vec<parent::Model>) {
|
||||
use crate::{models::candidate_details::tests::APPLICATION_DETAILS, services::parent_service::ParentService};
|
||||
|
||||
let plain_text_password = "test".to_string();
|
||||
|
||||
let secret_message = "trnka".to_string();
|
||||
|
||||
let candidate = CandidateService::create(&db, APPLICATION_ID, &plain_text_password, "".to_string())
|
||||
.await
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
Mutation::create_parent(&db, APPLICATION_ID).await.unwrap();
|
||||
|
||||
let encrypted_message =
|
||||
crypto::encrypt_password_with_recipients(&secret_message, &vec![&candidate.public_key])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let private_key_plain_text =
|
||||
crypto::decrypt_password(candidate.private_key, plain_text_password)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let decrypted_message =
|
||||
crypto::decrypt_password_with_private_key(&encrypted_message, &private_key_plain_text)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(secret_message, decrypted_message);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn create_admin(db: &DbConn) -> admin::Model {
|
||||
use chrono::Utc;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
|
||||
let password = "admin".to_string();
|
||||
let (pubkey, priv_key) = crypto::create_identity();
|
||||
let enc_priv_key = crypto::encrypt_password(priv_key, password).await.unwrap();
|
||||
|
||||
let admin = admin::ActiveModel {
|
||||
name: Set("admin".to_string()),
|
||||
public_key: Set(pubkey),
|
||||
private_key: Set(enc_priv_key),
|
||||
password: Set("admin".to_string()),
|
||||
created_at: Set(Utc::now().naive_utc()),
|
||||
updated_at: Set(Utc::now().naive_utc()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
admin
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn put_user_data(db: &DbConn) -> (candidate::Model, Vec<parent::Model>) {
|
||||
use crate::models::candidate_details::tests::APPLICATION_DETAILS;
|
||||
|
||||
let plain_text_password = "test".to_string();
|
||||
let (candidate, _parent) = ApplicationService::create_candidate_with_parent(
|
||||
&db,
|
||||
let application = ApplicationService::create(
|
||||
&"".to_string(),
|
||||
db,
|
||||
APPLICATION_ID,
|
||||
&plain_text_password,
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap();
|
||||
"0000001111".to_string()
|
||||
).await.unwrap();
|
||||
|
||||
let candidate= ApplicationService::find_related_candidate(db, &application).await.unwrap();
|
||||
ParentService::create(db, candidate.id).await.unwrap();
|
||||
|
||||
let form = APPLICATION_DETAILS.lock().unwrap().clone();
|
||||
|
||||
let (candidate, parents) = ApplicationService::add_all_details(&db, candidate, &form)
|
||||
let (candidate, parents) = ApplicationService::add_all_details(&db, &application, candidate, &form)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(
|
||||
application,
|
||||
candidate,
|
||||
parents,
|
||||
)
|
||||
|
|
@ -434,7 +120,7 @@ pub mod tests {
|
|||
#[tokio::test]
|
||||
async fn test_put_user_data() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let (candidate, parents) = put_user_data(&db).await;
|
||||
let (_, candidate, parents) = put_user_data(&db).await;
|
||||
assert!(candidate.name.is_some());
|
||||
assert!(parents[0].name.is_some());
|
||||
}
|
||||
|
|
@ -443,9 +129,9 @@ pub mod tests {
|
|||
async fn test_encrypt_decrypt_user_data() {
|
||||
let password = "test".to_string();
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let (enc_candidate, enc_parent) = put_user_data(&db).await;
|
||||
let (application, enc_candidate, enc_parent) = put_user_data(&db).await;
|
||||
|
||||
let dec_priv_key = crypto::decrypt_password(enc_candidate.private_key.clone(), password)
|
||||
let dec_priv_key = crypto::decrypt_password(application.private_key.clone(), password)
|
||||
.await
|
||||
.unwrap();
|
||||
let enc_details = EncryptedApplicationDetails::try_from((&enc_candidate, enc_parent))
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl ParentService {
|
|||
for i in 0..parents_details.len() {
|
||||
let found_parent = match found_parents.get(i) {
|
||||
Some(parent) => parent.to_owned(),
|
||||
None => ParentService::create(db, ref_candidate.application).await?,
|
||||
None => ParentService::create(db, ref_candidate.id).await?,
|
||||
};
|
||||
let enc_details = EncryptedParentDetails::new(&parents_details[i], recipients).await?;
|
||||
let parent = Mutation::add_parent_details(db, found_parent, enc_details.clone()).await?;
|
||||
|
|
@ -54,9 +54,7 @@ mod tests {
|
|||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
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 crate::{utils::db::get_memory_sqlite_connection, models::{candidate::{ParentDetails, ApplicationDetails, CandidateDetails}, candidate_details::EncryptedApplicationDetails}, services::{candidate_service::{CandidateService, tests::put_user_data}, application_service::ApplicationService, parent_service::ParentService}, crypto};
|
||||
|
||||
pub static APPLICATION_DETAILS_TWO_PARENTS: Lazy<Mutex<ApplicationDetails>> = Lazy::new(||
|
||||
Mutex::new(ApplicationDetails {
|
||||
|
|
@ -73,7 +71,6 @@ mod tests {
|
|||
personal_id_number: "personal_id_number".to_string(),
|
||||
school_name: "school_name".to_string(),
|
||||
health_insurance: "health_insurance".to_string(),
|
||||
study: "study".to_string(),
|
||||
},
|
||||
parents: vec![ParentDetails {
|
||||
name: "parent_name".to_string(),
|
||||
|
|
@ -93,34 +90,27 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn create_parent_test() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
CandidateService::create(&db, 103100, &"test".to_string(), "".to_string()).await.unwrap();
|
||||
super::ParentService::create(&db, 103100).await.unwrap();
|
||||
super::ParentService::create(&db, 103100).await.unwrap();
|
||||
let candidate = CandidateService::create(&db, "".to_string()).await.unwrap();
|
||||
super::ParentService::create(&db, candidate.id).await.unwrap();
|
||||
super::ParentService::create(&db, candidate.id).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_parent_details_test() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let plain_text_password = "test".to_string();
|
||||
let (candidate, _parent) = ApplicationService::create_candidate_with_parent(
|
||||
&db,
|
||||
103101,
|
||||
&plain_text_password,
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap();
|
||||
// let application = ApplicationService::create(&"".to_string(), &db, 103100, &plain_text_password, "".to_string()).await.unwrap();
|
||||
let (application, candidate, _) = put_user_data(&db).await;
|
||||
|
||||
ParentService::create(&db, 103101).await.unwrap();
|
||||
ParentService::create(&db, candidate.id).await.unwrap();
|
||||
|
||||
let form = APPLICATION_DETAILS_TWO_PARENTS.lock().unwrap().clone();
|
||||
|
||||
let (candidate, parents) = ApplicationService::add_all_details(&db, candidate, &form)
|
||||
let (candidate, parents) = ApplicationService::add_all_details(&db, &application, candidate, &form)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let priv_key = crypto::decrypt_password(candidate.private_key.clone(), plain_text_password).await.unwrap();
|
||||
let priv_key = crypto::decrypt_password(application.private_key.clone(), plain_text_password).await.unwrap();
|
||||
let dec_details = EncryptedApplicationDetails::try_from((&candidate, parents))
|
||||
.unwrap()
|
||||
.decrypt(priv_key)
|
||||
|
|
@ -136,8 +126,7 @@ mod tests {
|
|||
assert_eq!(dec_details.candidate.citizenship, form.candidate.citizenship);
|
||||
assert_eq!(dec_details.candidate.email, form.candidate.email);
|
||||
assert_eq!(dec_details.candidate.sex, form.candidate.sex);
|
||||
assert_eq!(dec_details.candidate.personal_id_number, form.candidate.personal_id_number);
|
||||
assert_eq!(dec_details.candidate.study, form.candidate.study);
|
||||
assert_eq!(dec_details.candidate.personal_id_number, "0000001111".to_string());
|
||||
|
||||
assert_eq!(dec_details.parents.len(), form.parents.len());
|
||||
for i in 0..dec_details.parents.len() {
|
||||
|
|
@ -146,8 +135,5 @@ mod tests {
|
|||
assert_eq!(dec_details.parents[i].telephone, form.parents[i].telephone);
|
||||
assert_eq!(dec_details.parents[i].email, form.parents[i].email);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|||
|
||||
use crate::{error::ServiceError, Query, crypto};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum SubmissionProgress {
|
||||
NoneInCache,
|
||||
SomeInCache(Vec<FileType>),
|
||||
|
|
@ -50,7 +50,7 @@ impl Serialize for SubmissionProgress {
|
|||
}
|
||||
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, PartialEq, Clone)]
|
||||
pub enum FileType {
|
||||
CoverLetterPdf = 1,
|
||||
PortfolioLetterPdf = 2,
|
||||
|
|
@ -216,17 +216,7 @@ impl PortfolioService {
|
|||
|
||||
/// Returns true if portfolio is ready to be moved to the final directory
|
||||
async fn is_portfolio_prepared(candidate_id: i32) -> bool {
|
||||
let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache");
|
||||
|
||||
let filenames = vec![FileType::CoverLetterPdf, FileType::PortfolioLetterPdf, FileType::PortfolioZip];
|
||||
for filename in filenames {
|
||||
if !tokio::fs::metadata(
|
||||
cache_path.join(filename.as_str())
|
||||
).await.is_ok() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
Self::get_submission_progress(candidate_id).await.ok() == Some(SubmissionProgress::AllInCache)
|
||||
}
|
||||
|
||||
// Delete single item from cache
|
||||
|
|
@ -269,7 +259,7 @@ impl PortfolioService {
|
|||
|
||||
/// Move files from cache to final directory and delete cache afterwards
|
||||
pub async fn submit(candidate: &candidate::Model, db: &DbConn) -> Result<(), ServiceError> {
|
||||
let candidate_id = candidate.application;
|
||||
let candidate_id = candidate.id;
|
||||
let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf();
|
||||
let cache_path = path.join("cache");
|
||||
|
||||
|
|
@ -277,7 +267,7 @@ impl PortfolioService {
|
|||
return Err(ServiceError::IncompletePortfolio);
|
||||
}
|
||||
|
||||
info!("PORTFOLIO {} SUBMIT STARTED", candidate.application);
|
||||
info!("PORTFOLIO {} SUBMIT STARTED", candidate.id);
|
||||
|
||||
let mut archive = tokio::fs::File::create(path.join(FileType::PortfolioZip.as_str())).await?;
|
||||
let mut writer = async_zip::write::ZipFileWriter::new(&mut archive);
|
||||
|
|
@ -306,11 +296,15 @@ impl PortfolioService {
|
|||
writer.close().await?;
|
||||
archive.shutdown().await?;
|
||||
|
||||
let applications_pubkeys: Vec<String> = Query::find_applications_by_candidate_id(db, candidate_id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|a| a.public_key.to_owned()).collect();
|
||||
let admin_public_keys = Query::get_all_admin_public_keys(db).await?;
|
||||
let candidate_public_key = &candidate.public_key;
|
||||
let mut admin_public_keys_refrence: Vec<&str> = admin_public_keys.iter().map(|s| &**s).collect();
|
||||
let mut recipients = vec![&**candidate_public_key];
|
||||
recipients.append(&mut admin_public_keys_refrence);
|
||||
|
||||
let mut recipients = vec![];
|
||||
recipients.append(&mut admin_public_keys.iter().map(|s| &**s).collect());
|
||||
recipients.append(&mut applications_pubkeys.iter().map(|s| &**s).collect());
|
||||
|
||||
let final_path = path.join(FileType::PortfolioZip.as_str());
|
||||
|
||||
|
|
@ -420,18 +414,17 @@ mod tests {
|
|||
#[serial]
|
||||
async fn test_folder_creation() {
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let plain_text_password = "test".to_string();
|
||||
|
||||
let temp_dir = std::env::temp_dir().join("portfolio_test_tempdir").join("create_folder");
|
||||
std::env::set_var("PORTFOLIO_STORE_PATH", temp_dir.to_str().unwrap());
|
||||
|
||||
CandidateService::create(&db, APPLICATION_ID, &plain_text_password, "".to_string())
|
||||
let candidate = CandidateService::create(&db, "".to_string())
|
||||
.await
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
assert!(tokio::fs::metadata(temp_dir.join(APPLICATION_ID.to_string())).await.is_ok());
|
||||
assert!(tokio::fs::metadata(temp_dir.join(APPLICATION_ID.to_string()).join("cache")).await.is_ok());
|
||||
assert!(tokio::fs::metadata(temp_dir.join(candidate.id.to_string())).await.is_ok());
|
||||
assert!(tokio::fs::metadata(temp_dir.join(candidate.id.to_string()).join("cache")).await.is_ok());
|
||||
|
||||
tokio::fs::remove_dir_all(temp_dir).await.unwrap();
|
||||
}
|
||||
|
|
@ -620,14 +613,14 @@ mod tests {
|
|||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_add_portfolio() {
|
||||
let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await;
|
||||
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let (candidate, _) = put_user_data(&db).await;
|
||||
let (_, candidate, _) = put_user_data(&db).await;
|
||||
|
||||
let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.id).await;
|
||||
|
||||
PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
|
||||
PortfolioService::submit(&candidate, &db).await.unwrap();
|
||||
|
||||
|
|
@ -639,20 +632,20 @@ mod tests {
|
|||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_delete_portfolio() {
|
||||
let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await;
|
||||
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let (candidate, _) = put_user_data(&db).await;
|
||||
let (_, candidate, _) = put_user_data(&db).await;
|
||||
|
||||
PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.id).await;
|
||||
|
||||
PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
|
||||
PortfolioService::submit(&candidate, &db).await.unwrap();
|
||||
|
||||
assert!(tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok());
|
||||
|
||||
PortfolioService::delete_portfolio(APPLICATION_ID).await.unwrap();
|
||||
PortfolioService::delete_portfolio(candidate.id).await.unwrap();
|
||||
|
||||
assert!(!tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok());
|
||||
|
||||
|
|
@ -662,32 +655,32 @@ mod tests {
|
|||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_is_portfolio_submitted() {
|
||||
let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await;
|
||||
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let (candidate, _) = put_user_data(&db).await;
|
||||
|
||||
PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
let (_, candidate, _) = put_user_data(&db).await;
|
||||
let (temp_dir, _, _) = create_data_store_temp_dir(candidate.id).await;
|
||||
|
||||
PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
|
||||
PortfolioService::submit(&candidate, &db).await.unwrap();
|
||||
|
||||
assert!(PortfolioService::is_portfolio_submitted(APPLICATION_ID).await);
|
||||
assert!(PortfolioService::is_portfolio_submitted(candidate.id).await);
|
||||
|
||||
clear_data_store_temp_dir(temp_dir).await;
|
||||
|
||||
let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await;
|
||||
let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.id).await;
|
||||
|
||||
PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap();
|
||||
PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]).await.unwrap();
|
||||
|
||||
PortfolioService::submit(&candidate, &db).await.unwrap();
|
||||
|
||||
tokio::fs::remove_file(application_dir.join("PORTFOLIO.age")).await.unwrap();
|
||||
|
||||
assert!(!PortfolioService::is_portfolio_submitted(APPLICATION_ID).await);
|
||||
assert!(!PortfolioService::is_portfolio_submitted(candidate.id).await);
|
||||
|
||||
clear_data_store_temp_dir(temp_dir).await;
|
||||
}
|
||||
|
|
@ -695,22 +688,22 @@ mod tests {
|
|||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_get_portfolio() {
|
||||
let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await;
|
||||
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
let (candidate, _parent) = put_user_data(&db).await;
|
||||
let (application, candidate, _parent) = put_user_data(&db).await;
|
||||
|
||||
let private_key = crypto::decrypt_password(candidate.private_key.clone(), "test".to_string())
|
||||
let (temp_dir, _, _) = create_data_store_temp_dir(candidate.id).await;
|
||||
|
||||
let private_key = crypto::decrypt_password(application.private_key.clone(), "test".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0])
|
||||
PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0])
|
||||
.await
|
||||
.unwrap();
|
||||
PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0])
|
||||
PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0])
|
||||
.await
|
||||
.unwrap();
|
||||
PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0])
|
||||
PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -718,7 +711,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
PortfolioService::get_portfolio(APPLICATION_ID, private_key)
|
||||
PortfolioService::get_portfolio(candidate.id, private_key)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -42,30 +42,21 @@ mod tests {
|
|||
|
||||
use crate::{
|
||||
crypto,
|
||||
services::{application_service::ApplicationService, candidate_service::CandidateService},
|
||||
services::{application_service::ApplicationService},
|
||||
utils::db::get_memory_sqlite_connection, models::auth::AuthenticableTrait,
|
||||
};
|
||||
const SECRET: &str = "Tajny_kod";
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_candidate() {
|
||||
const SECRET: &str = "Tajny_kod";
|
||||
|
||||
let db = get_memory_sqlite_connection().await;
|
||||
|
||||
let candidate = ApplicationService::create_candidate_with_parent(
|
||||
&db,
|
||||
103151,
|
||||
&SECRET.to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap()
|
||||
.0;
|
||||
let application = ApplicationService::create(&"".to_string(), &db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap();
|
||||
|
||||
assert_eq!(candidate.application, 103151);
|
||||
assert_ne!(candidate.code, SECRET.to_string());
|
||||
assert!(crypto::verify_password(SECRET.to_string(), candidate.code)
|
||||
assert_eq!(application.id.to_owned(), 103151);
|
||||
assert_ne!(application.password.to_owned(), SECRET.to_string());
|
||||
assert!(crypto::verify_password(SECRET.to_string(), application.password)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap());
|
||||
|
|
@ -75,28 +66,20 @@ mod tests {
|
|||
async fn test_candidate_session_correct_password() {
|
||||
let db = &get_memory_sqlite_connection().await;
|
||||
|
||||
let candidate = ApplicationService::create_candidate_with_parent(
|
||||
db,
|
||||
103151,
|
||||
&"Tajny_kod".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
let application = ApplicationService::create(&"".to_string(), &db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap();
|
||||
|
||||
// correct password
|
||||
let session = CandidateService::new_session(
|
||||
let session = ApplicationService::new_session(
|
||||
db,
|
||||
&candidate,
|
||||
"Tajny_kod".to_string(),
|
||||
&application,
|
||||
SECRET.to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// println!("{}", session.err().unwrap().1);
|
||||
assert!(
|
||||
CandidateService::auth(db, Uuid::parse_str(&session).unwrap())
|
||||
ApplicationService::auth(db, Uuid::parse_str(&session).unwrap())
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
|
@ -106,20 +89,12 @@ mod tests {
|
|||
async fn test_candidate_session_incorrect_password() {
|
||||
let db = &get_memory_sqlite_connection().await;
|
||||
|
||||
let candidate_form = ApplicationService::create_candidate_with_parent(
|
||||
&db,
|
||||
103151,
|
||||
&"Tajny_kod".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
let application = ApplicationService::create(&"".to_string(), &db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap();
|
||||
|
||||
// incorrect password
|
||||
assert!(CandidateService::new_session(
|
||||
assert!(ApplicationService::new_session(
|
||||
db,
|
||||
&candidate_form,
|
||||
&application,
|
||||
"Spatny_kod".to_string(),
|
||||
"127.0.0.1".to_string()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
use sea_orm::{DbConn};
|
||||
use crate::{error::ServiceError, models::candidate_details::{EncryptedApplicationDetails}, Query, models::candidate::{Row, ApplicationDetails}};
|
||||
use crate::{
|
||||
error::ServiceError,
|
||||
models::candidate_details::EncryptedApplicationDetails,
|
||||
models::{application::ApplicationRow, candidate::ApplicationDetails},
|
||||
Query, services::application_service::ApplicationService,
|
||||
};
|
||||
use sea_orm::DbConn;
|
||||
|
||||
impl From<(i32, ApplicationDetails)> for Row {
|
||||
impl From<(i32, ApplicationDetails)> for ApplicationRow {
|
||||
fn from((application, d): (i32, ApplicationDetails)) -> Self {
|
||||
let c = d.candidate;
|
||||
Self {
|
||||
|
|
@ -15,7 +20,6 @@ impl From<(i32, ApplicationDetails)> for Row {
|
|||
citizenship: Some(c.citizenship),
|
||||
email: Some(c.email),
|
||||
sex: Some(c.sex),
|
||||
study: Some(c.study),
|
||||
health_insurance: Some(c.health_insurance),
|
||||
school_name: Some(c.school_name),
|
||||
personal_identification_number: Some(c.personal_id_number),
|
||||
|
|
@ -33,33 +37,29 @@ impl From<(i32, ApplicationDetails)> for Row {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn export(
|
||||
db: &DbConn,
|
||||
private_key: String,
|
||||
) -> Result<Vec<u8>, ServiceError> {
|
||||
pub async fn export(db: &DbConn, private_key: String) -> Result<Vec<u8>, ServiceError> {
|
||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||
|
||||
let candidates_with_parents = Query::list_candidates_full(&db).await?;
|
||||
for candidate in candidates_with_parents {
|
||||
let application = candidate.application;
|
||||
let applications = Query::list_applications_compact(&db).await?;
|
||||
for application in applications {
|
||||
let candidate = ApplicationService::find_related_candidate(db, &application).await?;
|
||||
let parents = Query::find_candidate_parents(db, &candidate).await?;
|
||||
|
||||
let row: Row = match EncryptedApplicationDetails::try_from((&candidate, parents)) {
|
||||
Ok(d) => Row::from(
|
||||
d
|
||||
.decrypt(private_key.to_string())
|
||||
let row: ApplicationRow = match EncryptedApplicationDetails::try_from((&candidate, parents))
|
||||
{
|
||||
Ok(d) => ApplicationRow::from(
|
||||
d.decrypt(private_key.to_string())
|
||||
.await
|
||||
.map(|d| (application, d))?
|
||||
.map(|d| (application.id, d))?,
|
||||
),
|
||||
|
||||
Err(_) => Row {
|
||||
application,
|
||||
Err(_) => ApplicationRow {
|
||||
application: application.id,
|
||||
..Default::default()
|
||||
}
|
||||
},
|
||||
};
|
||||
wtr.serialize(row)?;
|
||||
}
|
||||
wtr
|
||||
.into_inner()
|
||||
wtr.into_inner()
|
||||
.map_err(|_| ServiceError::CsvIntoInnerError)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
core/src/utils/date.rs
Normal file
13
core/src/utils/date.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use chrono::NaiveDate;
|
||||
|
||||
use crate::error::ServiceError;
|
||||
|
||||
pub fn parse_naive_date_from_opt_str(date: Option<String>, fmt: &str) -> Result<NaiveDate, ServiceError> {
|
||||
Ok(
|
||||
NaiveDate::parse_from_str(&date.unwrap_or_default(), fmt)
|
||||
.unwrap_or(
|
||||
NaiveDate::from_ymd_opt(1, 1, 1)
|
||||
.ok_or(ServiceError::InvalidDate)?
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use entity::admin_session;
|
||||
use entity::{admin_session, application};
|
||||
use sea_orm::DbConn;
|
||||
|
||||
use crate::Query;
|
||||
|
|
@ -22,15 +22,17 @@ pub async fn get_memory_sqlite_connection() -> sea_orm::DbConn {
|
|||
|
||||
let schema = Schema::new(DbBackend::Sqlite);
|
||||
let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity);
|
||||
let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity);
|
||||
let stmt2: TableCreateStatement = schema.create_table_from_entity(application::Entity);
|
||||
let stmt3: TableCreateStatement = schema.create_table_from_entity(session::Entity);
|
||||
let stmt4: TableCreateStatement = schema.create_table_from_entity(parent::Entity);
|
||||
let stmt4: TableCreateStatement = schema.create_table_from_entity(admin::Entity);
|
||||
let stmt5: TableCreateStatement = schema.create_table_from_entity(admin_session::Entity);
|
||||
let stmt6: TableCreateStatement = schema.create_table_from_entity(parent::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.execute(db.get_database_backend().build(&stmt6)).await.unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
|
|
|
|||
0
core/src/utils/field_of_study.rs
Normal file
0
core/src/utils/field_of_study.rs
Normal file
|
|
@ -1,3 +1,5 @@
|
|||
pub mod csv;
|
||||
pub mod filetype;
|
||||
pub mod db;
|
||||
pub mod db;
|
||||
pub mod date;
|
||||
pub mod field_of_study;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "portfolio-entity"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::session_trait::UserSession;
|
|||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub admin_id: Option<i32>,
|
||||
pub admin_id: i32,
|
||||
pub ip_address: String,
|
||||
pub created_at: DateTime,
|
||||
pub expires_at: DateTime,
|
||||
|
|
|
|||
46
entity/src/application.rs
Normal file
46
entity/src/application.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "application")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: i32,
|
||||
pub candidate_id: i32,
|
||||
pub field_of_study: String,
|
||||
pub password: String,
|
||||
pub public_key: String,
|
||||
pub private_key: String,
|
||||
pub personal_id_number: String,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::candidate::Entity",
|
||||
from = "Column::CandidateId",
|
||||
to = "super::candidate::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Candidate,
|
||||
#[sea_orm(has_many = "super::session::Entity")]
|
||||
Session,
|
||||
}
|
||||
|
||||
impl Related<super::candidate::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Candidate.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::session::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Session.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
@ -5,9 +5,8 @@ use sea_orm::entity::prelude::*;
|
|||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "candidate")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub application: i32,
|
||||
pub code: String,
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: Option<String>,
|
||||
pub surname: Option<String>,
|
||||
pub birth_surname: Option<String>,
|
||||
|
|
@ -18,27 +17,25 @@ pub struct Model {
|
|||
pub citizenship: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub sex: Option<String>,
|
||||
pub study: Option<String>,
|
||||
pub personal_identification_number: String,
|
||||
pub school_name: Option<String>,
|
||||
pub health_insurance: Option<String>,
|
||||
pub public_key: String,
|
||||
pub private_key: String,
|
||||
pub encrypted_by_id: Option<i32>,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::session::Entity")]
|
||||
Session,
|
||||
#[sea_orm(has_many = "super::application::Entity")]
|
||||
Application,
|
||||
#[sea_orm(has_many = "super::parent::Entity")]
|
||||
Parent,
|
||||
}
|
||||
|
||||
impl Related<super::session::Entity> for Entity {
|
||||
impl Related<super::application::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Session.def()
|
||||
Relation::Application.def()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ pub mod candidate;
|
|||
pub mod parent;
|
||||
pub mod session;
|
||||
pub mod admin_session;
|
||||
pub mod session_trait;
|
||||
pub mod session_trait;
|
||||
pub mod application;
|
||||
|
|
@ -4,6 +4,7 @@ pub mod prelude;
|
|||
|
||||
pub mod admin;
|
||||
pub mod admin_session;
|
||||
pub mod application;
|
||||
pub mod candidate;
|
||||
pub mod parent;
|
||||
pub mod session;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use sea_orm::entity::prelude::*;
|
|||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub application: i32,
|
||||
pub candidate_id: i32,
|
||||
pub name: Option<String>,
|
||||
pub surname: Option<String>,
|
||||
pub telephone: Option<String>,
|
||||
|
|
@ -20,8 +20,8 @@ pub struct Model {
|
|||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::candidate::Entity",
|
||||
from = "Column::Application",
|
||||
to = "super::candidate::Column::Application",
|
||||
from = "Column::CandidateId",
|
||||
to = "super::candidate::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
pub use super::admin::Entity as Admin;
|
||||
pub use super::admin_session::Entity as AdminSession;
|
||||
pub use super::application::Entity as Application;
|
||||
pub use super::candidate::Entity as Candidate;
|
||||
pub use super::parent::Entity as Parent;
|
||||
pub use super::session::Entity as Session;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::session_trait::UserSession;
|
|||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub candidate_id: Option<i32>,
|
||||
pub candidate_id: i32,
|
||||
pub ip_address: String,
|
||||
pub created_at: DateTime,
|
||||
pub expires_at: DateTime,
|
||||
|
|
@ -19,18 +19,18 @@ pub struct Model {
|
|||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::candidate::Entity",
|
||||
belongs_to = "super::application::Entity",
|
||||
from = "Column::CandidateId",
|
||||
to = "super::candidate::Column::Application",
|
||||
to = "super::application::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Candidate,
|
||||
Application,
|
||||
}
|
||||
|
||||
impl Related<super::candidate::Entity> for Entity {
|
||||
impl Related<super::application::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Candidate.def()
|
||||
Relation::Application.def()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import axios, { type AxiosProgressEvent } from 'axios';
|
||||
import type { CandidateData, CandidateLogin, CreateCandidate } from '$lib/stores/candidate';
|
||||
import type { BaseCandidate, CandidateData, CandidateLogin, CreateCandidate } from '$lib/stores/candidate';
|
||||
import type { SubmissionProgress } from '$lib/stores/portfolio';
|
||||
import { API_URL, errorHandler, type Fetch } from '.';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
|
@ -51,7 +51,7 @@ export const apiFetchSubmissionProgress = async (fetchSsr?: Fetch): Promise<Subm
|
|||
}
|
||||
};
|
||||
|
||||
export const apiWhoami = async (fetchSsr?: Fetch): Promise<CreateCandidate> => {
|
||||
export const apiWhoami = async (fetchSsr?: Fetch): Promise<BaseCandidate> => {
|
||||
const apiFetch = fetchSsr || fetch;
|
||||
try {
|
||||
console.log(API_URL + '/candidate/whoami');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
|
||||
<script lang="ts">
|
||||
export let linkOk: boolean = false;
|
||||
export let linkError: boolean = false;
|
||||
export let applications: Array<number>;
|
||||
|
||||
let title1 = `Ano, podával/a jsem dvě přihlášky na dva obory SSPŠaG (${applications[0]} a ${applications[1]})`;
|
||||
let title2 = `Ne, přihlášku na SSPŠaG jsem podával/a jen jednu (${applications[0]})`;
|
||||
|
||||
if (applications.length === 1) {
|
||||
title1 = `Ano, přihlášku na SSPŠaG jsem podával/a jen jednu (${applications[0]})`;
|
||||
title2 = `Ne, přihlášku na SSPŠaG jsem podával více přihlášek`;
|
||||
}
|
||||
|
||||
|
||||
$: console.log(linkOk, linkError);
|
||||
|
||||
export let error: string = '';
|
||||
|
||||
const switchSelection = (id: number) => {
|
||||
if (id === 0) {
|
||||
linkOk = true;
|
||||
linkError = false;
|
||||
} else {
|
||||
linkOk = false;
|
||||
linkError = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<input
|
||||
on:click={(_) => switchSelection(0)}
|
||||
class:error
|
||||
on:change
|
||||
type="checkbox"
|
||||
id="linkOk"
|
||||
checked={linkOk}
|
||||
class="peer hidden"
|
||||
/>
|
||||
<label for="linkOk" class="peer-checked:border-sspsBlue peer-checked:text-gray-600" class:error>
|
||||
<div class="block">
|
||||
<span class="text-2xl">📜</span>
|
||||
|
||||
<div class="w-full text-lg font-semibold">
|
||||
{title1}
|
||||
</div>
|
||||
<div class="w-full text-sm">Vše je v pořádku</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
on:click={(_) => switchSelection(1)}
|
||||
on:change
|
||||
type="checkbox"
|
||||
id="linkError"
|
||||
checked={linkError}
|
||||
class="peer hidden"
|
||||
/>
|
||||
<label
|
||||
for="linkError"
|
||||
class="peer-checked:border-sspsBlue peer-checked:text-gray-600"
|
||||
>
|
||||
<div class="block">
|
||||
<span class="text-2xl">📜</span>
|
||||
|
||||
<div class="w-full text-lg font-semibold">
|
||||
{title2}
|
||||
</div>
|
||||
<div class="w-full text-sm">Co se děje?</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
label {
|
||||
@apply inline-flex w-full items-center justify-between;
|
||||
@apply cursor-pointer;
|
||||
@apply bg-white p-5 text-gray-500;
|
||||
@apply hover:bg-gray-50 hover:text-gray-600;
|
||||
@apply rounded-lg border-2 border-gray-200;
|
||||
}
|
||||
.error {
|
||||
@apply border-red-700;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -65,6 +65,16 @@
|
|||
}
|
||||
};
|
||||
|
||||
const getField = (id: number) => {
|
||||
if (id.toString().startsWith("101")) {
|
||||
return 'G';
|
||||
} else if (id.toString().startsWith("102")) {
|
||||
return 'IT';
|
||||
} else {
|
||||
return 'KB';
|
||||
}
|
||||
};
|
||||
|
||||
const editDetails = async () => {
|
||||
goto('/register?edit=true');
|
||||
};
|
||||
|
|
@ -140,9 +150,15 @@
|
|||
class="mt-4 flex flex-col justify-between leading-10"
|
||||
>
|
||||
<span
|
||||
>Ev. č. přihlášky: <span class="font-bold">{$baseCandidateData.applicationId}</span
|
||||
></span
|
||||
>Ev. č. přihlášky ({getField($baseCandidateData.applications[0])}):
|
||||
<span class="font-bold">{$baseCandidateData.applications[0]}</span></span
|
||||
>
|
||||
{#if $baseCandidateData.applications.length > 1}
|
||||
<span
|
||||
>Ev. č. přihlášky ({getField($baseCandidateData.applications[1])}):
|
||||
<span class="font-bold">{$baseCandidateData.applications[1]}</span></span
|
||||
>
|
||||
{/if}
|
||||
<span>Obor: <span class="font-bold">{$candidateData.candidate.study}</span></span>
|
||||
<span>Adresa: <span class="font-bold">{$candidateData.candidate.address}</span></span>
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -41,13 +41,23 @@ export interface CreateCandidate {
|
|||
personalIdNumber: string;
|
||||
}
|
||||
|
||||
export interface BaseCandidate {
|
||||
currentApplication: number;
|
||||
applications: Array<number>;
|
||||
personalIdNumber: string;
|
||||
detailsFilled: boolean;
|
||||
encryptedBy?: number;
|
||||
}
|
||||
|
||||
export interface CreateCandidateLogin extends CreateCandidate {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export const baseCandidateData = writable<CreateCandidate>({
|
||||
applicationId: 0,
|
||||
personalIdNumber: ''
|
||||
export const baseCandidateData = writable<BaseCandidate>({
|
||||
currentApplication: 0,
|
||||
applications: [],
|
||||
personalIdNumber: '',
|
||||
detailsFilled: false
|
||||
});
|
||||
|
||||
export const candidateData = writable<CandidateData>({
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@
|
|||
import type { Writable } from 'svelte/store';
|
||||
import * as yup from 'yup';
|
||||
import type { CandidateData } from '$lib/stores/candidate';
|
||||
import AccountLinkCheckBox from '$lib/components/checkbox/AccountLinkCheckBox.svelte';
|
||||
|
||||
const pageCount = 5;
|
||||
const pageCount = 6;
|
||||
let pageIndex = 0;
|
||||
let pagesFilled = [false, false, false, false, false];
|
||||
let pagesFilled = [false, false, false, false, false, false];
|
||||
let pageTexts = [
|
||||
'Zpracování osobních údajů',
|
||||
'Registrace',
|
||||
|
|
@ -35,9 +36,15 @@
|
|||
|
||||
export let data: PageData;
|
||||
let details = data.candidate;
|
||||
let baseCandidateDetails = data.whoami;
|
||||
|
||||
let detailsFilledByAnotherAccount = baseCandidateDetails.encryptedBy !== null &&
|
||||
baseCandidateDetails.currentApplication !== baseCandidateDetails.encryptedBy;
|
||||
|
||||
const formInitialValues = {
|
||||
gdpr: false,
|
||||
linkOk: false,
|
||||
linkError: false,
|
||||
candidate: {
|
||||
name: '',
|
||||
surname: '',
|
||||
|
|
@ -75,6 +82,8 @@
|
|||
|
||||
const formValidationSchema = yup.object().shape({
|
||||
gdpr: yup.boolean().oneOf([true]),
|
||||
linkOk: yup.boolean().oneOf([true]),
|
||||
linkError: yup.boolean().oneOf([false]),
|
||||
candidate: yup.object().shape({
|
||||
name: yup.string().required(),
|
||||
surname: yup.string().required(),
|
||||
|
|
@ -265,11 +274,16 @@
|
|||
const isPageInvalid = (index: number): boolean => {
|
||||
switch (index) {
|
||||
case 0:
|
||||
if ($typedErrors['gdpr']) {
|
||||
if ($typedErrors['linkOk'] || $typedErrors['linkError']) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if ($typedErrors['gdpr']) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (
|
||||
$typedErrors['candidate']['name'] ||
|
||||
$typedErrors['candidate']['surname'] ||
|
||||
|
|
@ -280,7 +294,7 @@
|
|||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
case 3:
|
||||
if (
|
||||
$typedErrors['candidate']['birthplace'] ||
|
||||
$typedErrors['candidate']['birthdate'] ||
|
||||
|
|
@ -293,7 +307,7 @@
|
|||
return true;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
if (
|
||||
$typedErrors['parents'][0]['name'] ||
|
||||
$typedErrors['parents'][0]['surname'] ||
|
||||
|
|
@ -303,7 +317,7 @@
|
|||
return true;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
case 5:
|
||||
if (
|
||||
$typedErrors['parents'][1]['name'] ||
|
||||
$typedErrors['parents'][1]['surname'] ||
|
||||
|
|
@ -313,7 +327,7 @@
|
|||
return true;
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
case 6:
|
||||
if (
|
||||
$typedErrors['candidate']['citizenship'] ||
|
||||
$typedErrors['candidate']['personalIdNumber'] ||
|
||||
|
|
@ -343,6 +357,8 @@
|
|||
);
|
||||
form.set({
|
||||
gdpr: true,
|
||||
linkOk: true,
|
||||
linkError: false,
|
||||
candidate: {
|
||||
...details.candidate,
|
||||
street: details.candidate.address.split(',')[0].split(' ')[0],
|
||||
|
|
@ -364,8 +380,8 @@
|
|||
}
|
||||
]
|
||||
});
|
||||
pageIndex = 1; // skip gdpr page
|
||||
pageTexts[1] = 'Úprava osobních údajů';
|
||||
pageIndex = 2; // skip gdpr page
|
||||
pageTexts[2] = 'Úprava osobních údajů';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -378,6 +394,23 @@
|
|||
</div>
|
||||
<form on:submit={handleSubmit} id="triggerForm" class="invisible hidden" />
|
||||
{#if pageIndex === 0}
|
||||
<form on:submit={handleSubmit}>
|
||||
<h1 class="title mt-8">Propojení účtů</h1>
|
||||
<p class="description mt-8 block text-center">
|
||||
Elektronickou přihlášky stačí vyplnit jen jednou i v případě, že jste podali dvě přihlášky.
|
||||
Potvrďte, že jste jste k nám skutečně podali dvě přihlášky.
|
||||
</p>
|
||||
<div class="field">
|
||||
<AccountLinkCheckBox
|
||||
applications={baseCandidateDetails.applications}
|
||||
bind:linkOk={$form.linkOk}
|
||||
bind:linkError={$form.linkError}
|
||||
on:change={handleChange}
|
||||
error={$typedErrors['linkOk']}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
{:else if pageIndex === 1}
|
||||
<form on:submit={handleSubmit}>
|
||||
<h1 class="title mt-8">{pageTexts[0]}</h1>
|
||||
<p class="description mt-8 block text-center">
|
||||
|
|
@ -394,7 +427,7 @@
|
|||
/>
|
||||
</div>
|
||||
</form>
|
||||
{:else if pageIndex === 1}
|
||||
{:else if pageIndex === 2}
|
||||
<form on:submit={handleSubmit}>
|
||||
<h1 class="title mt-8">{pageTexts[1]}</h1>
|
||||
<p class="description mt-8 block text-center">
|
||||
|
|
@ -429,7 +462,7 @@
|
|||
</span>
|
||||
</div>
|
||||
</form>
|
||||
{:else if pageIndex === 2}
|
||||
{:else if pageIndex === 3}
|
||||
<h1 class="title mt-8">{pageTexts[2]}</h1>
|
||||
<p class="description mt-8 block text-center">
|
||||
Pro registraci je potřeba vyplnit několik údajů o Vás. Tyto údaje budou použity pro
|
||||
|
|
@ -505,7 +538,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if pageIndex === 3}
|
||||
{:else if pageIndex === 4}
|
||||
<h1 class="title mt-8">{pageTexts[3]}</h1>
|
||||
<p class="description mt-8 block text-center">
|
||||
Sběr dat o zákonném zástupci je klíčový pro získání důležitých kontaktů a informací.
|
||||
|
|
@ -537,7 +570,7 @@
|
|||
/>
|
||||
</span>
|
||||
</div>
|
||||
{:else if pageIndex === 4}
|
||||
{:else if pageIndex === 5}
|
||||
<h1 class="title mt-8">{pageTexts[4]}</h1>
|
||||
<p class="description mt-8 block text-center">
|
||||
Zde můžete zadat údaje o druhém zákonném zástupci. Škole tím umožníte lépe komunikovat.
|
||||
|
|
@ -569,7 +602,7 @@
|
|||
/>
|
||||
</span>
|
||||
</div>
|
||||
{:else if pageIndex === 5}
|
||||
{:else if pageIndex === 6}
|
||||
<h1 class="title mt-8">{pageTexts[5]}</h1>
|
||||
<p class="description mt-8 block text-center">
|
||||
Zadejte prosím své občanství, rodné číslo, či jeho alternativu Vaší země a obor na který
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "portfolio-migration"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ 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;
|
||||
mod m20230114_114628_create_application;
|
||||
mod m20230114_114826_create_application_candidate_fk;
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
|
@ -20,6 +22,7 @@ impl MigratorTrait for Migrator {
|
|||
Box::new(m20221024_124701_create_parent::Migration),
|
||||
Box::new(m20221025_154422_create_session::Migration),
|
||||
Box::new(m20221221_162232_create_admin_session::Migration),
|
||||
Box::new(m20230114_114628_create_application::Migration),
|
||||
];
|
||||
|
||||
if cfg!(debug_assertions) || cfg!(test) {
|
||||
|
|
@ -36,6 +39,7 @@ impl MigratorTrait for Migrator {
|
|||
migrations.push(Box::new(
|
||||
m20221028_194728_session_create_admin_fk::Migration,
|
||||
));
|
||||
migrations.push(Box::new(m20230114_114826_create_application_candidate_fk::Migration));
|
||||
}
|
||||
|
||||
migrations
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@ impl MigrationTrait for Migration {
|
|||
.table(Candidate::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Candidate::Application)
|
||||
ColumnDef::new(Candidate::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
.unique_key(),
|
||||
.auto_increment(),
|
||||
)
|
||||
.col(ColumnDef::new(Candidate::Code).string().not_null())
|
||||
.col(ColumnDef::new(Candidate::Name).string())
|
||||
.col(ColumnDef::new(Candidate::Surname).string())
|
||||
.col(ColumnDef::new(Candidate::BirthSurname).string())
|
||||
|
|
@ -29,12 +28,10 @@ impl MigrationTrait for Migration {
|
|||
.col(ColumnDef::new(Candidate::Citizenship).string())
|
||||
.col(ColumnDef::new(Candidate::Email).string())
|
||||
.col(ColumnDef::new(Candidate::Sex).string())
|
||||
.col(ColumnDef::new(Candidate::Study).string())
|
||||
.col(ColumnDef::new(Candidate::PersonalIdentificationNumber).string().not_null())
|
||||
.col(ColumnDef::new(Candidate::SchoolName).string())
|
||||
.col(ColumnDef::new(Candidate::HealthInsurance).string())
|
||||
.col(ColumnDef::new(Candidate::PublicKey).string().not_null())
|
||||
.col(ColumnDef::new(Candidate::PrivateKey).string().not_null())
|
||||
.col(ColumnDef::new(Candidate::EncryptedById).integer())
|
||||
.col(ColumnDef::new(Candidate::CreatedAt).date_time().not_null())
|
||||
.col(ColumnDef::new(Candidate::UpdatedAt).date_time().not_null())
|
||||
.to_owned(),
|
||||
|
|
@ -52,8 +49,7 @@ impl MigrationTrait for Migration {
|
|||
#[derive(Iden)]
|
||||
pub enum Candidate {
|
||||
Table,
|
||||
Application,
|
||||
Code,
|
||||
Id,
|
||||
Name,
|
||||
Surname,
|
||||
BirthSurname,
|
||||
|
|
@ -64,12 +60,10 @@ pub enum Candidate {
|
|||
Citizenship,
|
||||
Email,
|
||||
Sex,
|
||||
Study,
|
||||
PersonalIdentificationNumber,
|
||||
SchoolName,
|
||||
HealthInsurance,
|
||||
PublicKey,
|
||||
PrivateKey,
|
||||
EncryptedById,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ impl MigrationTrait for Migration {
|
|||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Parent::Application)
|
||||
ColumnDef::new(Parent::CandidateId)
|
||||
.integer()
|
||||
.not_null()
|
||||
)
|
||||
|
|
@ -45,7 +45,7 @@ impl MigrationTrait for Migration {
|
|||
pub enum Parent {
|
||||
Id,
|
||||
Table,
|
||||
Application,
|
||||
CandidateId,
|
||||
Name,
|
||||
Surname,
|
||||
Telephone,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ impl MigrationTrait for Migration {
|
|||
.unique_key()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Session::CandidateId).integer())
|
||||
.col(ColumnDef::new(Session::CandidateId).integer().not_null())
|
||||
.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())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use crate::{m20221025_154422_create_session::Session, m20221024_121621_create_candidate::Candidate};
|
||||
use crate::{m20221025_154422_create_session::Session, m20230114_114628_create_application::Application};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
|
@ -11,7 +11,7 @@ impl MigrationTrait for Migration {
|
|||
manager.create_foreign_key(ForeignKey::create()
|
||||
.name("user_fk")
|
||||
.from(Session::Table, Session::CandidateId)
|
||||
.to(Candidate::Table, Candidate::Application)
|
||||
.to(Application::Table, Application::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.to_owned()).await
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ impl MigrationTrait for Migration {
|
|||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager.create_foreign_key(ForeignKey::create()
|
||||
.name("candidate_fk")
|
||||
.from(Parent::Table, Parent::Application)
|
||||
.to(Candidate::Table, Candidate::Application)
|
||||
.from(Parent::Table, Parent::CandidateId)
|
||||
.to(Candidate::Table, Candidate::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.to_owned()).await
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ impl MigrationTrait for Migration {
|
|||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(AdminSession::AdminId).integer())
|
||||
.col(ColumnDef::new(AdminSession::AdminId).integer().not_null())
|
||||
.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())
|
||||
|
|
|
|||
63
migration/src/m20230114_114628_create_application.rs
Normal file
63
migration/src/m20230114_114628_create_application.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
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(Application::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Application::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Application::FieldOfStudy).string().not_null())
|
||||
.col(ColumnDef::new(Application::CandidateId).integer().not_null())
|
||||
.col(ColumnDef::new(Application::Password).string().not_null())
|
||||
.col(ColumnDef::new(Application::PublicKey).string().not_null())
|
||||
.col(ColumnDef::new(Application::PrivateKey).string().not_null())
|
||||
.col(ColumnDef::new(Application::PersonalIdNumber).string().not_null())
|
||||
.col(ColumnDef::new(Application::CreatedAt).date_time().not_null())
|
||||
.col(ColumnDef::new(Application::UpdatedAt).date_time().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager.create_index(
|
||||
Index::create()
|
||||
.name("idx_application_candidate_id")
|
||||
.table(Application::Table)
|
||||
.col(Application::CandidateId)
|
||||
.to_owned(),
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Application::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
pub enum Application {
|
||||
Table,
|
||||
Id,
|
||||
FieldOfStudy,
|
||||
Password,
|
||||
PersonalIdNumber,
|
||||
PublicKey,
|
||||
PrivateKey,
|
||||
CandidateId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use crate::{m20221024_121621_create_candidate::Candidate, m20230114_114628_create_application::Application};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager.create_foreign_key(ForeignKey::create()
|
||||
.name("candidate_fk")
|
||||
.from(Application::Table, Application::CandidateId)
|
||||
.to(Candidate::Table, Candidate::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.to_owned()).await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager.drop_foreign_key(ForeignKey::drop()
|
||||
.name("candidate_fk")
|
||||
.table(Application::Table)
|
||||
.to_owned()).await
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue