Merge remote-tracking branch 'origin' into admin_create_more_data

This commit is contained in:
Sebastian Pravda 2023-01-25 21:57:01 +01:00
commit c5ac32d1fd
No known key found for this signature in database
GPG key ID: F3BC84F08EFA3F57
15 changed files with 353 additions and 61 deletions

View file

@ -200,6 +200,19 @@ pub fn create_identity() -> (String, String) {
)
}
pub async fn encrypt_buffer_with_recipients(
input_buffer: &[u8],
recipients: &Vec<String>,
) -> Result<Vec<u8>, ServiceError> {
let mut output_buffer = vec![];
age_encrypt_with_recipients(input_buffer,
&mut output_buffer,
&recipients.iter().map(|s| s.as_str()).collect()
).await?;
Ok(output_buffer)
}
async fn age_encrypt_with_recipients<W: tokio::io::AsyncWrite + Unpin>(
input_buffer: &[u8],
output_buffer: &mut W,

View file

@ -122,7 +122,7 @@ mod tests {
let encrypted_details: EncryptedApplicationDetails = EncryptedApplicationDetails::new(
&APPLICATION_DETAILS.lock().unwrap().clone(),
vec!["age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_string()],
&vec!["age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_string()],
).await.unwrap();
let candidate = Mutation::update_candidate_opt_details(&db, candidate, encrypted_details.candidate, 1).await.unwrap();

View file

@ -77,7 +77,7 @@ mod tests {
let encrypted_details: EncryptedApplicationDetails = EncryptedApplicationDetails::new(
&APPLICATION_DETAILS.lock().unwrap().clone(),
vec!["age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_string()],
&vec!["age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_string()],
)
.await
.unwrap();

View file

@ -321,7 +321,7 @@ impl From<&parent::Model> for EncryptedParentDetails {
impl EncryptedApplicationDetails {
pub async fn new(
form: &ApplicationDetails,
recipients: Vec<String>,
recipients: &Vec<String>,
) -> Result<EncryptedApplicationDetails, ServiceError> {
let candidate = EncryptedCandidateDetails::new(&form.candidate, &recipients).await?;
let enc_parents = future::try_join_all(
@ -459,7 +459,7 @@ pub mod tests {
async fn test_encrypted_application_details_new() {
let encrypted_details = EncryptedApplicationDetails::new(
&APPLICATION_DETAILS.lock().unwrap().clone(),
vec![PUBLIC_KEY.to_string()],
&vec![PUBLIC_KEY.to_string()],
)
.await
.unwrap();
@ -488,7 +488,7 @@ pub mod tests {
async fn test_encrypted_application_details_decrypt() {
let encrypted_details = EncryptedApplicationDetails::new(
&APPLICATION_DETAILS.lock().unwrap().clone(),
vec![PUBLIC_KEY.to_string()],
&vec![PUBLIC_KEY.to_string()],
)
.await
.unwrap();

View file

@ -6,7 +6,7 @@ use sea_orm::{DbConn, prelude::Uuid, IntoActiveModel};
use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::EncryptedApplicationDetails, models::{candidate::{ApplicationDetails, CreateCandidateResponse}, candidate_details::{EncryptedString, EncryptedCandidateDetails}, auth::AuthenticableTrait, application::ApplicationResponse}, Mutation, crypto::{hash_password, self}};
use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService, portfolio_service::PortfolioService};
use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService, portfolio_service::{PortfolioService, SubmissionProgress}};
const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"];
@ -281,8 +281,6 @@ impl ApplicationService {
).await
})
).await
}
async fn decrypt_private_key(
@ -307,7 +305,6 @@ impl ApplicationService {
}
}
// TODO
pub async fn reset_password(
admin_private_key: String,
db: &DbConn,
@ -316,7 +313,6 @@ impl ApplicationService {
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?;
@ -335,6 +331,7 @@ impl ApplicationService {
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)
@ -346,25 +343,19 @@ impl ApplicationService {
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))
.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()
let candidate = Self::update_all_application_details(db,
application.id,
candidate,
&recipients,
&admin_private_key
).await?;
Mutation::update_candidate_opt_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?;
if PortfolioService::get_submission_progress(candidate.id).await? == SubmissionProgress::Submitted {
PortfolioService::reencrypt_portfolio(
candidate.id,
admin_private_key,
&recipients
).await?;
}
Ok(
@ -379,6 +370,37 @@ impl ApplicationService {
}
)
}
async fn update_all_application_details(db: &DbConn,
application_id: i32,
candidate: candidate::Model,
recipients: &Vec<String>,
admin_private_key: &String
) -> Result<candidate::Model, ServiceError> {
let parents = Query::find_candidate_parents(db, &candidate).await?;
let dec_details = EncryptedApplicationDetails::from((&candidate, &parents))
.decrypt(admin_private_key.to_owned()).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?;
let candidate = Mutation::update_candidate_opt_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(candidate)
}
}
#[async_trait]
@ -477,7 +499,6 @@ mod application_tests {
assert!(!ApplicationService::is_application_id_valid(101));
}
// TODO
#[tokio::test]
async fn test_password_reset() {
let db = get_memory_sqlite_connection().await;

View file

@ -368,15 +368,46 @@ impl PortfolioService {
/// Returns decrypted portfolio zip as Vec of bytes
pub async fn get_portfolio(candidate_id: i32, private_key: String) -> Result<Vec<u8>, ServiceError> {
info!("PORTFOLIO {} DECRYPT STARTED", candidate_id);
let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf();
let path = path.join(FileType::Age.as_str());
let path = Self::get_file_store_path()
.join(&candidate_id.to_string())
.join(FileType::Age.as_str())
.to_path_buf();
let buffer = crypto::decrypt_file_with_private_key_as_buffer(path, &private_key).await?;
info!("PORTFOLIO {} DECRYPT FINISHED", candidate_id);
Ok(buffer)
}
pub async fn reencrypt_portfolio(candidate_id: i32,
private_key: String,
recipients: &Vec<String>
) -> Result<(), ServiceError> {
info!("PORTFOLIO {} REENCRYPT STARTED", candidate_id);
let path = Self::get_file_store_path()
.join(&candidate_id.to_string())
.join(FileType::Age.as_str())
.to_path_buf();
let plain_portfolio = crypto::decrypt_file_with_private_key_as_buffer(
path.to_owned(),
&private_key
).await?;
let enc_portfolio= crypto::encrypt_buffer_with_recipients(
&plain_portfolio,
recipients
).await?;
tokio::fs::remove_file(path.to_owned()).await?;
tokio::fs::write(path, enc_portfolio).await?;
info!("PORTFOLIO {} REENCRYPT FINISHED", candidate_id);
Ok(())
}
}
#[cfg(test)]

View file

@ -1,4 +1,6 @@
<script lang="ts">
import LL from '$i18n/i18n-svelte';
import { fetchSubmProgress } from '$lib/stores/portfolio';
import { apiDeleteCoverLetter, apiUploadCoverLetter } from '$lib/@api/candidate';
import DashboardUploadCard from './DashboardUploadCard.svelte';
@ -28,9 +30,9 @@
{error}
on:filedrop={(e) => onFileDrop(e.detail)}
on:delete={onDelete}
title="Motivační dopis"
title={$LL.components.dashboard.coverLetterUploadCard.title()}
filetype="PDF"
filesize={10}
fileType={1}
placeholder="svůj motivanční dopis"
placeholder={$LL.components.dashboard.coverLetterUploadCard.placeholder()}
/>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import LL from '$i18n/i18n-svelte';
import debounce from 'just-debounce-it';
import {
@ -7,7 +9,6 @@
apiLogout,
apiSubmitPortfolio
} from '$lib/@api/candidate';
import Circles from '$lib/components/icons/Circles.svelte';
import { fetchSubmProgress, type Status } from '$lib/stores/portfolio';
import StatusNotificationBig from './StatusNotificationBig.svelte';
import InfoButton from './InfoButton.svelte';
@ -150,39 +151,39 @@
class="mt-4 flex flex-col justify-between leading-10"
>
<span
>Ev. č. přihlášky ({getField($baseCandidateData.applications[0])}):
>{$LL.input.evidenceNumber()} ({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])}):
>{$LL.input.evidenceNumber()} ({getField($baseCandidateData.applications[1])}):
<span class="font-bold">{$baseCandidateData.applications[1]}</span></span
>
{/if}
<span>Adresa: <span class="font-bold">{$candidateData.candidate.address}</span></span>
<span>{$LL.input.address()}: <span class="font-bold">{$candidateData.candidate.address}</span></span>
<span
>Datum narození: <span class="font-bold">{$candidateData.candidate.birthdate}</span
>{$LL.input.birthDate()}: <span class="font-bold">{$candidateData.candidate.birthdate}</span
></span
>
<span
>Místo narození: <span class="font-bold">{$candidateData.candidate.birthplace}</span
>{$LL.input.birthPlace()}: <span class="font-bold">{$candidateData.candidate.birthplace}</span
></span
>
<span
>Rodné číslo: <span class="font-bold"
>{$LL.input.personalIdentificationNumber()}: <span class="font-bold"
>{$candidateData.candidate.personalIdNumber}</span
></span
>
<span
>IČO/Název školy: <span class="font-bold">{$candidateData.candidate.schoolName}</span
>{$LL.input.schoolIzo()}: <span class="font-bold">{$candidateData.candidate.schoolName}</span
></span
>
<span
>Číslo zdravotní pojišťovny: <span class="font-bold"
>{$LL.input.insuranceNumber()}: <span class="font-bold"
>{$candidateData.candidate.healthInsurance}</span
></span
>
<span>Telefon: <span class="font-bold">{$candidateData.candidate.telephone}</span></span
<span>{$LL.input.telephone()}: <span class="font-bold">{$candidateData.candidate.telephone}</span></span
>
</div>
<div
@ -200,8 +201,8 @@
<span class="text-sspsBlue text-xl font-bold"
>{parent.name + ' ' + parent.surname}</span
>
<span>Email: <span class="font-bold">{parent.email}</span></span>
<span>Telefon: <span class="font-bold">{parent.telephone}</span></span>
<span>{$LL.input.email()}: <span class="font-bold">{parent.email}</span></span>
<span>{$LL.input.telephone()}: <span class="font-bold">{parent.telephone}</span></span>
</div>
{/each}
</div>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import LL from '$i18n/i18n-svelte';
import FileType from './FileType.svelte';
import debounce from 'just-debounce-it';
import { filedrop, type FileDropOptions, type Files } from 'filedrop-svelte';
@ -103,7 +105,7 @@
{#if status === 'uploaded'}
<button
class="mr-3 rounded-xl bg-[#ef8b46] py-0.5 px-2 text-white shadow-md transition-all duration-300 hover:bg-orange-400"
on:click={debounce(() => dispatch('delete'), 150)}>Smazat</button
on:click={debounce(() => dispatch('delete'), 150)}>{$LL.components.dashboard.dashboardUploadCard.delete()}</button
>
{/if}
<StatusNotificationDot {status} />
@ -131,9 +133,9 @@
>
<div class="hidden items-center xl:block">
{#if bytesTotal === 0 || Math.round(progress * 100) === 100}
<h2 class="text-xl font-bold">{status === 'submitted' ? 'Odesláno' : 'Nahráno'}</h2>
<h2 class="text-xl font-bold">{status === 'submitted' ? $LL.components.dashboard.dashboardUploadCard.sent() : $LL.components.dashboard.dashboardUploadCard.uploaded()}</h2>
{:else}
<h2 class="text-xl">Nahráno {((bytesTotal / 1_000_000) * progress).toFixed(1)} MB</h2>
<h2 class="text-xl">{$LL.components.dashboard.dashboardUploadCard.uploaded()} {((bytesTotal / 1_000_000) * progress).toFixed(1)} MB</h2>
<h2 class="self-center text-xl">z {(bytesTotal / 1_000_000).toFixed(1)} MB</h2>
{/if}
</div>
@ -168,8 +170,14 @@
{#if error}
<span class="font-semibold text-red-600">{error}</span>
{:else}
<span class="text-[#406280]">Sem přetáhněte,</span>
<span class="text-sspsGray">nebo nahrajte {placeholder}</span>
<span class="text-[#406280]"
>{$LL.components.dashboard.dashboardUploadCard.dropHere()}</span
>
<span class="text-sspsGray"
>{$LL.components.dashboard.dashboardUploadCard.orUpload({
placeholder
})}</span
>
{/if}
</div>
</div>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import LL from '$i18n/i18n-svelte';
import type { ApiError } from '$lib/@api';
import { fetchSubmProgress } from '$lib/stores/portfolio';
import { apiDeletePortfolioLetter, apiUploadPortfolioLetter } from '../../@api/candidate';
@ -28,9 +30,9 @@
{error}
on:filedrop={(e) => onFileDrop(e.detail)}
on:delete={onDelete}
title="Portfolio"
title={$LL.components.dashboard.portfolioLetterUploadCard.title()}
filetype="PDF"
filesize={10}
fileType={2}
placeholder="svoje portfolio"
placeholder={$LL.components.dashboard.portfolioLetterUploadCard.placeholder()}
/>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import LL from '$i18n/i18n-svelte';
import { fetchSubmProgress } from '$lib/stores/portfolio';
import { apiDeletePortfolioZip, apiUploadPortfolioZip } from '$lib/@api/candidate';
import DashboardUploadCard from './DashboardUploadCard.svelte';
@ -28,9 +30,9 @@
{error}
on:filedrop={(e) => onFileDrop(e.detail)}
on:delete={onDelete}
title="Další data"
title={$LL.components.dashboard.portfolioZipUploadCard.title()}
filetype="ZIP"
filesize={100}
fileType={3}
placeholder="vaše další soubory ve formátu ZIP"
placeholder={$LL.components.dashboard.portfolioZipUploadCard.placeholder()}
/>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import LL from '$i18n/i18n-svelte';
import type { Status } from '$lib/stores/portfolio';
export let loading: boolean = false;
@ -8,16 +10,16 @@
let description: string;
$: switch (status) {
case 'submitted':
title = 'Soubory odevzdány!';
description = 'Vaše soubory smažete kliknutím zde';
title = $LL.components.dashboard.statusNotificationBig.submitted.title();
description = $LL.components.dashboard.statusNotificationBig.submitted.description();
break;
case 'uploaded':
title = 'Soubory nebyly odevzdány!';
description = 'Odevzdejte soubory kliknutím zde';
title = $LL.components.dashboard.statusNotificationBig.uploaded.title();
description = $LL.components.dashboard.statusNotificationBig.uploaded.description();
break;
case 'missing':
title = 'Soubory nebyly nahrány!';
description = 'Nahrajte včechny soubory prosím';
title = $LL.components.dashboard.statusNotificationBig.missing.title();
description = $LL.components.dashboard.statusNotificationBig.missing.description();
break;
}
</script>

View file

@ -423,6 +423,7 @@
<form on:submit={handleSubmit} id="triggerForm" class="invisible hidden" />
{#if pageIndex === 0}
<form on:submit={handleSubmit}>
<h1 class="title mt-8">{$LL.candidate.register.first.title()}</h1>
<h1 class="title mt-8">{$LL.candidate.register.first.title()}</h1>
<p class="description mt-8 block text-center">
{$LL.candidate.register.first.description()}
@ -467,6 +468,7 @@
<h1 class="title mt-8">{pageTexts[1]}</h1>
<p class="description mt-8 block text-center">
{$LL.candidate.register.third.description()}
{$LL.candidate.register.third.description()}
</p>
<div class="w-full">
<div class="flex flex-col">

View file

@ -69,6 +69,41 @@ const cs: BaseTranslation = {
}
},
components: {
dashboard: {
coverLetterUploadCard: {
title: 'Motivační dopis',
placeholder: 'svůj motivanční dopis'
},
portfolioLetterUploadCard: {
title: 'Portfolio',
placeholder: 'své portfolio'
},
portfolioZipUploadCard: {
title: 'Další data',
placeholder: 'vaše další soubory ve formátu ZIP'
},
dashboardUploadCard: {
dropHere: 'Sem přetáhněte,',
orUpload: 'Nebo nahrajte {placeholder:string}',
uploaded: 'Nahráno',
sent: 'Odesláno',
delete: 'Smazat'
},
statusNotificationBig: {
submitted: {
title: 'Soubory odevzdány!',
description: 'Vaše soubory smažete kliknutím zde'
},
uploaded: {
title: 'Soubory nebyly odevzdány!',
description: 'Odevzdejte soubory kliknutím zde'
},
missing: {
title: 'Soubory nebyly nahrány!',
description: 'Nahrajte včechny soubory prosím',
}
}
},
checkbox: {
accountLinkCheckBox: {
ok: 'Vše je v pořádku',
@ -117,7 +152,7 @@ const cs: BaseTranslation = {
adminId: 'Admin Id',
password: 'Heslo',
submit: 'Odeslat',
continue: "Pokračovat",
continue: 'Pokračovat',
parent: {
nameSurname: 'Jméno a příjmení zákonného zástupce',
email: 'E-mail zákonného zástupce',

View file

@ -150,6 +150,93 @@ type RootTranslation = {
}
}
components: {
dashboard: {
coverLetterUploadCard: {
/**
* Motivační dopis
*/
title: string
/**
* svůj motivanční dopis
*/
placeholder: string
}
portfolioLetterUploadCard: {
/**
* Portfolio
*/
title: string
/**
* své portfolio
*/
placeholder: string
}
portfolioZipUploadCard: {
/**
* Další data
*/
title: string
/**
* vaše další soubory ve formátu ZIP
*/
placeholder: string
}
dashboardUploadCard: {
/**
* Sem přetáhněte,
*/
dropHere: string
/**
* Nebo nahrajte {placeholder}
* @param {string} placeholder
*/
orUpload: RequiredParams<'placeholder'>
/**
* Nahráno
*/
uploaded: string
/**
* Odesláno
*/
sent: string
/**
* Smazat
*/
'delete': string
}
statusNotificationBig: {
submitted: {
/**
* Soubory odevzdány!
*/
title: string
/**
* Vaše soubory smažete kliknutím zde
*/
description: string
}
uploaded: {
/**
* Soubory nebyly odevzdány!
*/
title: string
/**
* Odevzdejte soubory kliknutím zde
*/
description: string
}
missing: {
/**
* Soubory nebyly nahrány!
*/
title: string
/**
* Nahrajte včechny soubory prosím
*/
description: string
}
}
}
checkbox: {
accountLinkCheckBox: {
/**
@ -461,6 +548,92 @@ export type TranslationFunctions = {
}
}
components: {
dashboard: {
coverLetterUploadCard: {
/**
* Motivační dopis
*/
title: () => LocalizedString
/**
* svůj motivanční dopis
*/
placeholder: () => LocalizedString
}
portfolioLetterUploadCard: {
/**
* Portfolio
*/
title: () => LocalizedString
/**
* své portfolio
*/
placeholder: () => LocalizedString
}
portfolioZipUploadCard: {
/**
* Další data
*/
title: () => LocalizedString
/**
* vaše další soubory ve formátu ZIP
*/
placeholder: () => LocalizedString
}
dashboardUploadCard: {
/**
* Sem přetáhněte,
*/
dropHere: () => LocalizedString
/**
* Nebo nahrajte {placeholder}
*/
orUpload: (arg: { placeholder: string }) => LocalizedString
/**
* Nahráno
*/
uploaded: () => LocalizedString
/**
* Odesláno
*/
sent: () => LocalizedString
/**
* Smazat
*/
'delete': () => LocalizedString
}
statusNotificationBig: {
submitted: {
/**
* Soubory odevzdány!
*/
title: () => LocalizedString
/**
* Vaše soubory smažete kliknutím zde
*/
description: () => LocalizedString
}
uploaded: {
/**
* Soubory nebyly odevzdány!
*/
title: () => LocalizedString
/**
* Odevzdejte soubory kliknutím zde
*/
description: () => LocalizedString
}
missing: {
/**
* Soubory nebyly nahrány!
*/
title: () => LocalizedString
/**
* Nahrajte včechny soubory prosím
*/
description: () => LocalizedString
}
}
}
checkbox: {
accountLinkCheckBox: {
/**