Compare commits

..
6 Commits
Author SHA1 Message Date
Lucas Vas 6c3371e472 Merge pull request #83 from R15-Software-Dev/fix/centralize-cognito-pool-ids
Removes duplicated magic strings across multiple files for Cognito pools and the like. These strings are alright to be public-facing.
2026-09-24 12:20:32 -04:00
alphafetus 988f5c83b7 fix: remove duplicated magic strings across multiple files 2026-09-24 12:04:12 -04:00
Lucas Vas 2e0e47d72b Merge pull request #81 from R15-Software-Dev/fix/admin-file-errors
Insert trailing commas after lists of strings
2026-04-27 15:02:16 -04:00
alphafetus 287598bb79 fix: insert trailing commas after lists of strings
This was already present for lists of dictionaries, but lists that contained a single string also failed to parse properly.
2026-04-27 14:57:31 -04:00
Lucas Vas fff1854067 Merge pull request #80 from R15-Software-Dev/fix/admin-file-errors
Allow essays and fafsa to be requested by administrators
2026-04-27 14:24:20 -04:00
alphafetus 6d6862a680 fix: allow essays and fafsa to be requested by administrators 2026-04-27 14:08:14 -04:00
13 changed files with 96 additions and 64 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
pub const STUDENT_PROVIDER_POOL_ID: &str = "us-east-1_Lfjuy5zaM";
pub const PROVIDER_GROUP: &str = "ScholarshipProviders";
pub const STUDENT_GROUP: &str = "ScholarshipStudents";
+4 -3
View File
@@ -4,6 +4,7 @@ use aws_config::BehaviorVersion;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use aws_sdk_cognitoidentityprovider::error::ProvideErrorMetadata;
use crate::constants::{PROVIDER_GROUP, STUDENT_GROUP, STUDENT_PROVIDER_POOL_ID};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
@@ -43,7 +44,7 @@ async fn add_user_to_group(provider: Client, user_id: String, group_name: impl I
println!("Adding user {} to group {}", user_id, group_name);
match provider
.admin_add_user_to_group()
.user_pool_id("us-east-1_Lfjuy5zaM")
.user_pool_id(STUDENT_PROVIDER_POOL_ID)
.username(user_id)
.group_name(group_name)
.send()
@@ -63,8 +64,8 @@ pub(crate) async fn handler(event: LambdaEvent<PostConfirmationEvent>) -> Result
// Check the client ID of the request to assign the user to the correct location.
let res = match event.payload.caller_context.client_id.as_str() {
"10jr2h3vtpu9n7gj46pvg5qo2q" => add_user_to_group(client, user_id, "ScholarshipStudents").await,
"56c2bqvl021rv8d5mq36blt7jv" => add_user_to_group(client, user_id, "ScholarshipProviders").await,
"10jr2h3vtpu9n7gj46pvg5qo2q" => add_user_to_group(client, user_id, STUDENT_GROUP).await,
"56c2bqvl021rv8d5mq36blt7jv" => add_user_to_group(client, user_id, PROVIDER_GROUP).await,
_ => Err("Invalid client ID, denying all user access.".to_string()),
};
+1
View File
@@ -1,4 +1,5 @@
use lambda_runtime::{run, service_fn, tracing, Error};
mod constants;
mod handler;
use handler::handler as function_handler;
+5
View File
@@ -0,0 +1,5 @@
pub const STUDENT_PROVIDER_POOL_ID: &str = "us-east-1_Lfjuy5zaM";
pub const ADMIN_POOL_ID: &str = "us-east-1_rvCU4Xy4j";
pub const COGNITO_REGION: &str = "us-east-1";
pub const PROVIDER_GROUP: &str = "ScholarshipProviders";
pub const STUDENT_GROUP: &str = "ScholarshipStudents";
+2 -1
View File
@@ -9,8 +9,9 @@ mod value_type;
pub mod animations;
mod tab_info;
mod schema_node;
mod constants;
pub use self::{
comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*,
submit_status::*, user::*, value_type::*, tab_info::*, schema_node::*,
submit_status::*, user::*, value_type::*, tab_info::*, schema_node::*, constants::*,
};
+3 -2
View File
@@ -1,11 +1,12 @@
use crate::components::login::setup::provide_auth_context;
use crate::common::{ADMIN_POOL_ID, COGNITO_REGION};
use crate::components::login::setup::provide_auth_context;
use leptos::prelude::*;
#[component]
pub fn AdminLoginContext(children: Children) -> impl IntoView {
provide_auth_context(
crate::utils::use_origin(),
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_rvCU4Xy4j",
format!("https://cognito-idp.{COGNITO_REGION}.amazonaws.com/{ADMIN_POOL_ID}"),
"1vh1q994rid6cgi6tjf58r9jp4",
"/admin/callback",
);
+2 -1
View File
@@ -1,6 +1,7 @@
use leptos::prelude::*;
use leptos_router::components::Outlet;
use super::setup::provide_auth_context;
use crate::common::{COGNITO_REGION, STUDENT_PROVIDER_POOL_ID};
///# Provider Login Context Component
///
@@ -19,7 +20,7 @@ use super::setup::provide_auth_context;
pub fn ProviderLoginContext() -> impl IntoView {
provide_auth_context(
crate::utils::use_origin(),
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
format!("https://cognito-idp.{COGNITO_REGION}.amazonaws.com/{STUDENT_PROVIDER_POOL_ID}"),
"56c2bqvl021rv8d5mq36blt7jv",
"/providers/callback"
);
+2 -1
View File
@@ -1,5 +1,6 @@
use leptos::prelude::*;
use super::setup::provide_auth_context;
use crate::common::{COGNITO_REGION, STUDENT_PROVIDER_POOL_ID};
///# Student Login Context Component
///
@@ -20,7 +21,7 @@ pub fn StudentLoginContext(
) -> impl IntoView {
provide_auth_context(
crate::utils::use_origin(),
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
format!("https://cognito-idp.{COGNITO_REGION}.amazonaws.com/{STUDENT_PROVIDER_POOL_ID}"),
"10jr2h3vtpu9n7gj46pvg5qo2q",
"/students/callback"
);
+2 -1
View File
@@ -63,10 +63,11 @@ fn AdminApplicantsScholarshipList() -> impl IntoView {
async fn admin_get_provider_scholarships(
access_token: String,
) -> Result<Vec<ExpandableInfo>, ServerFnError> {
use crate::common::{ADMIN_POOL_ID, COGNITO_REGION};
use crate::pages::api::SCHOLARSHIPS_TABLE;
use crate::pages::api::tokens::validate_and_get_token_info;
let claims = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?;
let claims = validate_and_get_token_info(access_token, ADMIN_POOL_ID, COGNITO_REGION).await?;
// Get the information from the database.
let client = crate::utils::server::create_dynamo_client().await;
+4 -3
View File
@@ -6,6 +6,7 @@ use server_fn::codec::{MultipartData, MultipartFormData};
#[cfg(feature = "ssr")]
mod imports {
pub use super::super::{MAIN_TABLE_NAME, S3_BUCKET_NAME};
pub use crate::common::{COGNITO_REGION, STUDENT_PROVIDER_POOL_ID};
pub use crate::pages::api::tokens::validate_and_get_token_info;
pub use crate::utils::server::create_aws_config;
pub use aws_sdk_dynamodb::{
@@ -66,7 +67,7 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
return Err(ServerFnError::new("Missing file contents"));
}
let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
let user_claims = validate_and_get_token_info(access_token, STUDENT_PROVIDER_POOL_ID, COGNITO_REGION).await?;
let subject = user_claims.subject;
let key = format!("{form_id}/{subject}/{input_name}/{file_name}");
@@ -157,7 +158,7 @@ pub async fn delete_file(
) -> Result<String, ServerFnError> {
use imports::*;
let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
let user_claims = validate_and_get_token_info(access_token, STUDENT_PROVIDER_POOL_ID, COGNITO_REGION).await?;
let subject = user_claims.subject;
let entry_hk = format!("STUDENT#{subject}");
@@ -226,7 +227,7 @@ pub async fn list_files(
) -> Result<Vec<String>, ServerFnError> {
use imports::*;
let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
let user_claims = validate_and_get_token_info(access_token, STUDENT_PROVIDER_POOL_ID, COGNITO_REGION).await?;
let subject = user_claims.subject;
let entry_hk = format!("STUDENT#{subject}");
+49 -37
View File
@@ -7,16 +7,16 @@ use std::process::Stdio;
#[cfg(feature = "ssr")]
mod imports {
pub use super::super::MAIN_TABLE_NAME;
pub use crate::common::ValueType;
pub use crate::common::{ADMIN_POOL_ID, COGNITO_REGION, PROVIDER_GROUP, STUDENT_PROVIDER_POOL_ID, ValueType};
pub use crate::pages::api::tokens::validate_and_get_token_info;
pub use crate::utils::server::*;
pub use aws_sdk_dynamodb::error::ProvideErrorMetadata;
pub use aws_sdk_dynamodb::types::AttributeValue;
pub use leptos::logging::{debug_log, error};
pub use std::collections::HashMap;
pub use zip::write::SimpleFileOptions;
pub use leptos::serde_json;
pub use std::collections::HashMap;
pub use std::process::Command;
pub use zip::write::SimpleFileOptions;
}
static PDF_TEMPLATE: &str = include_str!("../../../pdf_template.typ");
@@ -147,11 +147,12 @@ pub async fn provider_get_completed_students(
) -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
use imports::*;
let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
return Err(ServerFnError::new(
"User is not in the ScholarshipProviders group",
));
let claims =
validate_and_get_token_info(access_token, STUDENT_PROVIDER_POOL_ID, COGNITO_REGION).await?;
if !claims.groups.contains(&PROVIDER_GROUP.to_string()) {
return Err(ServerFnError::new(format!(
"User is not in the {PROVIDER_GROUP} group",
)));
}
get_completed_students().await
@@ -163,13 +164,14 @@ pub async fn admin_get_completed_students(
) -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
use imports::*;
let _ = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?;
let _ = validate_and_get_token_info(access_token, ADMIN_POOL_ID, COGNITO_REGION).await?;
get_completed_students().await
}
#[server]
async fn get_completed_students() -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
async fn get_completed_students()
-> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
use imports::*;
// We want to get all student information. The requirements are that the students have completed
@@ -251,7 +253,8 @@ pub async fn get_file_by_key(
) -> Result<Vec<u8>, ServerFnError> {
use imports::*;
let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
let user_claims =
validate_and_get_token_info(access_token, STUDENT_PROVIDER_POOL_ID, COGNITO_REGION).await?;
// Check if the user's subject is contained in the file_key (since all files are keyed by the
// user's subject)
@@ -259,7 +262,7 @@ pub async fn get_file_by_key(
// Check that the user is a provider.
if !user_claims
.groups
.contains(&"ScholarshipProviders".to_string())
.contains(&PROVIDER_GROUP.to_string())
{
return Err(ServerFnError::new(
"Access denied: user is not the student or a provider",
@@ -301,12 +304,19 @@ pub async fn get_student_files(
) -> Result<(String, Vec<u8>), ServerFnError> {
use imports::*;
let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
// Check if the user has permission to get these files.
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
return Err(ServerFnError::new("Access denied: user is not a provider"));
}
match validate_and_get_token_info(access_token.clone(), STUDENT_PROVIDER_POOL_ID, COGNITO_REGION)
.await
{
Ok(claims) => {
if !claims.groups.contains(&PROVIDER_GROUP.to_string()) {
return Err(ServerFnError::new("Access denied: user is not a provider"));
}
}
Err(_) => {
let _ = validate_and_get_token_info(access_token, ADMIN_POOL_ID, COGNITO_REGION)
.await?;
}
};
let client = create_dynamo_client().await;
@@ -497,7 +507,7 @@ pub async fn admin_get_all_input_files(
) -> Result<HashMap<String, Vec<String>>, ServerFnError> {
use imports::*;
let _ = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?;
let _ = validate_and_get_token_info(access_token, ADMIN_POOL_ID, COGNITO_REGION).await?;
get_all_input_files(form_name, input_name).await
}
@@ -510,8 +520,9 @@ pub async fn provider_get_all_input_files(
) -> Result<HashMap<String, Vec<String>>, ServerFnError> {
use imports::*;
let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
let claims =
validate_and_get_token_info(access_token, STUDENT_PROVIDER_POOL_ID, COGNITO_REGION).await?;
if !claims.groups.contains(&PROVIDER_GROUP.to_string()) {
return Err(ServerFnError::new("Access denied: user is not a provider"));
}
@@ -519,9 +530,7 @@ pub async fn provider_get_all_input_files(
}
#[server]
pub async fn get_student_pdf(
student_id: String,
) -> Result<(String, Vec<u8>), ServerFnError> {
pub async fn get_student_pdf(student_id: String) -> Result<(String, Vec<u8>), ServerFnError> {
use imports::*;
// NOTE: this API requires that the server has the typst-cli available ON PATH.
@@ -529,11 +538,13 @@ pub async fn get_student_pdf(
let student_info_str = get_student_info_json(student_id.clone()).await?;
let student_info = get_student_data(student_id, "DEMOGRAPHICS".to_string()).await?;
let first_name = student_info.get("first_name")
let first_name = student_info
.get("first_name")
.map(|v| v.to_string())
.unwrap_or_default();
let last_name = student_info.get("last_name")
let last_name = student_info
.get("last_name")
.map(|v| v.to_string())
.unwrap_or_default();
@@ -543,7 +554,8 @@ pub async fn get_student_pdf(
.replace("[", "(")
.replace("]", ")")
// Ensures trailing commas for lists of dictionaries
.replace("))", "),)");
.replace("))", "),)")
.replace("\")", "\",)");
debug_log!("New string: {}", typst_string);
@@ -577,9 +589,7 @@ pub async fn get_student_pdf(
}
#[server]
pub async fn get_student_info_json(
student_id: String,
) -> Result<String, ServerFnError> {
pub async fn get_student_info_json(student_id: String) -> Result<String, ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
@@ -613,7 +623,8 @@ pub async fn get_student_info_json(
.map(|output| {
let items = output.items.unwrap_or_default();
debug_log!("Number of items: {}", items.len());
items.into_iter()
items
.into_iter()
.next()
.unwrap_or_default()
.get("extracurricular")
@@ -637,7 +648,8 @@ pub async fn get_student_info_json(
.map(|output| {
let items = output.items.unwrap_or_default();
debug_log!("Number of items: {}", items.len());
items.into_iter()
items
.into_iter()
.next()
.unwrap_or_default()
.get("extracurricular")
@@ -650,12 +662,12 @@ pub async fn get_student_info_json(
};
// Join items together into a single object.
let mut data = items.into_iter()
.fold(HashMap::new(), |mut map, item| {
item.into_iter()
.for_each(|(k, v)| { map.insert(k, v); });
map
let mut data = items.into_iter().fold(HashMap::new(), |mut map, item| {
item.into_iter().for_each(|(k, v)| {
map.insert(k, v);
});
map
});
data.insert("work_experience".to_string(), work_exp);
data.insert("extracurricular".to_string(), extracurricular);
-1
View File
@@ -28,7 +28,6 @@ pub async fn validate_and_get_token_info(
leptos::logging::debug_log!("Validating token using group {}", pool_id);
// let jwks = fetch_jwks("us-east-1_Lfjuy5zaM".into(), "us-east-1".into()).await?;
let jwks = fetch_jwks(pool_id, pool_region.into()).await?;
let validation = Validation::new(Algorithm::RS256);
+19 -14
View File
@@ -1,8 +1,13 @@
use crate::common::{ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType};
use crate::common::{
ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode,
SchemaType, ValueType,
};
use crate::components::{ActionButton, Banner, DataDisplay, Loading};
use crate::pages::UnauthenticatedPage;
use crate::pages::api::get_comparison_info;
use crate::pages::api::students::{GetStudentFiles, provider_get_all_input_files, get_student_data, GetStudentPdf};
use crate::pages::api::students::{
GetStudentFiles, GetStudentPdf, get_student_data, provider_get_all_input_files,
};
use base64::Engine;
use leptos::ev::{Event, MouseEvent};
use leptos::html::Dialog;
@@ -68,12 +73,18 @@ fn ApplicantsScholarshipList() -> impl IntoView {
#[server]
async fn get_provider_scholarships(
access_token: String
access_token: String,
) -> Result<Vec<ExpandableInfo>, ServerFnError> {
use crate::common::{COGNITO_REGION, STUDENT_PROVIDER_POOL_ID};
use crate::pages::api::SCHOLARSHIPS_TABLE;
use crate::pages::api::tokens::validate_and_get_token_info;
let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM".to_string(), "us-east-1".to_string()).await?;
let claims = validate_and_get_token_info(
access_token,
STUDENT_PROVIDER_POOL_ID.to_string(),
COGNITO_REGION.to_string(),
)
.await?;
// Get the information from the database.
let client = crate::utils::server::create_dynamo_client().await;
@@ -108,9 +119,7 @@ fn ApplicantsScholarshipList() -> impl IntoView {
let scholarships_res = Resource::new(
move || (trigger.track(), access_token.get()),
move |(_, access_token)| async move {
get_provider_scholarships(
access_token.unwrap_or_default()
).await
get_provider_scholarships(access_token.unwrap_or_default()).await
},
);
@@ -1121,9 +1130,8 @@ pub fn StudentInformationDialog(
let get_student_files = ServerAction::<GetStudentFiles>::new();
let get_student_pdf = ServerAction::<GetStudentPdf>::new();
let get_buttons_disabled = Memo::new(move |_| {
get_student_files.pending().get() && get_student_pdf.pending().get()
});
let get_buttons_disabled =
Memo::new(move |_| get_student_files.pending().get() && get_student_pdf.pending().get());
let on_click_fafsa = move |_| {
get_student_files.dispatch(GetStudentFiles {
access_token: access_token.get().unwrap_or_default(),
@@ -1310,10 +1318,7 @@ pub fn StudentInformationDialog(
})
})}
{move || Suspend::new(async move {
get_student_data(
student_id.get().unwrap_or_default(),
"family".to_string()
)
get_student_data(student_id.get().unwrap_or_default(), "family".to_string())
.await
.map(|student_info| {
view! {