feat: implement file upload functionality
This commit is contained in:
1 parent
64554d81bc
commit
40d1ef2a5f
10 files changed
+948
-42
No files matched your search
@@ -1,5 +1,5 @@
|
||||
use crate::common::{DateInfo, DateRange};
|
||||
use crate::components::{ActionButton, FileDrop};
|
||||
use crate::components::ActionButton;
|
||||
use crate::pages::api::exports::get_scholarship_csv;
|
||||
use crate::pages::api::{CreateDates, CreateTestComparisons, exports::GetScholarshipCsv};
|
||||
use chrono::DateTime;
|
||||
@@ -109,7 +109,6 @@ pub fn AdminUtilsPage() -> impl IntoView {
|
||||
<ActionButton on:click=on_click_comparisons>"Create Comparisons"</ActionButton>
|
||||
<ActionButton on:click=on_click_dates>"Create Dates"</ActionButton>
|
||||
<ActionButton on:click=on_click_export>"Scholarship Export"</ActionButton>
|
||||
<FileDrop />
|
||||
</div>
|
||||
</div>
|
||||
}.into_any()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
use leptos::prelude::*;
|
||||
use server_fn::codec::{MultipartFormData, MultipartData};
|
||||
use leptos::logging::debug_log;
|
||||
|
||||
#[server(input = MultipartFormData)]
|
||||
pub async fn upload_file(
|
||||
data: MultipartData
|
||||
) -> Result<(), ServerFnError> {
|
||||
use std::path::PathBuf;
|
||||
use crate::pages::api::tokens::validate_and_get_token_info;
|
||||
use crate::utils::server::create_aws_config;
|
||||
|
||||
let mut multipart = data.into_inner().unwrap();
|
||||
|
||||
let mut file_name = String::new();
|
||||
let mut file_bytes = Vec::<u8>::new();
|
||||
let mut form_id = String::new();
|
||||
let mut access_token = String::new();
|
||||
|
||||
while let Ok(Some(field)) = multipart.next_field().await {
|
||||
let field_name = field.name().unwrap_or_default();
|
||||
|
||||
match field_name {
|
||||
"access_token" => access_token = field.text().await?,
|
||||
"form_id" => form_id = field.text().await?,
|
||||
_ => {
|
||||
// NOTE: If there are multiple files put here the function will only upload the last
|
||||
// one in the data.
|
||||
// We should likely use chunk streaming in the future, for safety.
|
||||
file_name = field.file_name()
|
||||
.ok_or(ServerFnError::new("Couldn't find file name"))?
|
||||
.to_owned();
|
||||
file_bytes = field.bytes().await?.to_vec();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if access_token.is_empty() { return Err(ServerFnError::new("Missing access token")); }
|
||||
if form_id.is_empty() { return Err(ServerFnError::new("Missing form ID")); }
|
||||
if file_name.is_empty() { return Err(ServerFnError::new("Missing file")); }
|
||||
|
||||
let user_claims = validate_and_get_token_info(access_token).await?;
|
||||
let subject = user_claims.subject;
|
||||
|
||||
let key = format!("{form_id}/{subject}/{file_name}");
|
||||
|
||||
debug_log!("Adding file to S3: {:?}", key);
|
||||
|
||||
let client = aws_sdk_s3::Client::new(&create_aws_config().await);
|
||||
|
||||
client
|
||||
.put_object()
|
||||
.bucket("leptos-scholarships")
|
||||
.key(key)
|
||||
.body(file_bytes.into())
|
||||
.send()
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
@@ -5,6 +5,8 @@ pub mod exports;
|
||||
mod providers;
|
||||
mod scholarships;
|
||||
pub mod students;
|
||||
pub mod files;
|
||||
mod tokens;
|
||||
|
||||
pub use admin::*;
|
||||
pub use comparisons::*;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use jsonwebtoken::{jwk::JwkSet, DecodingKey};
|
||||
use leptos::prelude::*;
|
||||
use crate::common::UserClaims;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn fetch_jwks(user_pool_id: String, region: String) -> Result<JwkSet, ServerFnError> {
|
||||
let request_url = format!(
|
||||
"https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json",
|
||||
region,
|
||||
user_pool_id
|
||||
);
|
||||
|
||||
let jwks = reqwest::get(&request_url).await?
|
||||
.json::<JwkSet>()
|
||||
.await?;
|
||||
|
||||
Ok(jwks)
|
||||
}
|
||||
|
||||
/// Validates a JWT, and, if successful, decodes it into a series of user claims.
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn validate_and_get_token_info(token: String) -> Result<UserClaims, ServerFnError> {
|
||||
use jsonwebtoken::{decode, Algorithm, Validation};
|
||||
|
||||
let jwks = fetch_jwks("us-east-1_Lfjuy5zaM".into(), "us-east-1".into()).await?;
|
||||
|
||||
let validation = Validation::new(Algorithm::RS256);
|
||||
// validation.set_audience(&["scholarships-rs"]);
|
||||
|
||||
let header = jsonwebtoken::decode_header(&token)
|
||||
.map_err(|e| ServerFnError::new(format!("Invalid token header: {e}")))?;
|
||||
|
||||
let kid = header.kid.ok_or_else(|| ServerFnError::new("Missing kid header"))?;
|
||||
|
||||
let jwk = jwks.find(&kid)
|
||||
.ok_or_else(|| ServerFnError::new("Couldn't find matching key in JWKS"))?;
|
||||
|
||||
let token_data = decode::<UserClaims>(
|
||||
&token,
|
||||
&DecodingKey::from_jwk(&jwk)?,
|
||||
&validation
|
||||
).map_err(|_| ServerFnError::new("Invalid token found".to_string()))?;
|
||||
|
||||
if token_data.claims.token_use != "access" {
|
||||
return Err(ServerFnError::new("Token type mismatch"));
|
||||
}
|
||||
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
mod about_page;
|
||||
mod api;
|
||||
pub mod api;
|
||||
mod comparison_test;
|
||||
mod home_page;
|
||||
mod loaner_page;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::FileDrop;
|
||||
|
||||
#[component]
|
||||
pub fn AdditionalPage() -> impl IntoView {
|
||||
@@ -8,6 +9,8 @@ pub fn AdditionalPage() -> impl IntoView {
|
||||
<div class="text-lg">
|
||||
"This is just a placeholder for now. We'll show redirect buttons to each form."
|
||||
</div>
|
||||
<div class="text-lg">"Here's a testing file input!"</div>
|
||||
<FileDrop name="testing" form_id="student_additional"/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user