feat: implement file upload functionality

This commit is contained in:
alphafetus committed 2026-03-01 12:17:20 -05:00
1 parent 64554d81bc
commit 40d1ef2a5f
10 files changed
+948 -42

No files matched your search

+60
View File
@@ -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)
}
+2
View File
@@ -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::*;
+50
View File
@@ -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)
}