From 09de9fcf9114a4f6aff0687b9d9bdccc18e9da28 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 15 Jan 2026 09:48:21 -0500 Subject: [PATCH] feat: implement provider and student specific login contexts --- src/app.rs | 89 +++++++++++++------------------- src/components/login/mod.rs | 5 ++ src/components/login/provider.rs | 30 +++++++++++ src/components/login/setup.rs | 24 +++++++++ src/components/login/student.rs | 30 +++++++++++ src/components/mod.rs | 1 + src/lib.rs | 1 + src/pages/api/comparisons.rs | 4 +- src/pages/api/scholarships.rs | 3 +- src/pages/comparison_test.rs | 2 +- src/pages/home_page.rs | 2 +- src/pages/mod.rs | 1 - src/pages/provider_contact.rs | 11 ++-- src/pages/scholarship_info.rs | 2 +- src/pages/utils.rs | 42 --------------- src/utils/common.rs | 33 ++++++++++++ src/utils/mod.rs | 5 ++ src/utils/server.rs | 17 ++++++ 18 files changed, 193 insertions(+), 109 deletions(-) create mode 100644 src/components/login/mod.rs create mode 100644 src/components/login/provider.rs create mode 100644 src/components/login/setup.rs create mode 100644 src/components/login/student.rs delete mode 100644 src/pages/utils.rs create mode 100644 src/utils/common.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/server.rs diff --git a/src/app.rs b/src/app.rs index 09b05f0..bb3064d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,10 @@ use crate::pages::{AboutPage, HomePage, ProviderPortal, ComparisonTestPage, ScholarshipInfoPage, TestPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage}; use leptos::prelude::*; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; -use leptos_oidc::{Auth, AuthParameters, AuthSignal, Challenge}; -use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path}; +use leptos_oidc::AuthSignal; +use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path, MatchNestedRoutes}; +use leptos_router::any_nested_route::IntoAnyNestedRoute; +use crate::components::login::ProviderLoginContext; use crate::components::ToastList; pub fn shell(options: LeptosOptions) -> impl IntoView { @@ -43,54 +45,15 @@ pub fn App() -> impl IntoView { } } -fn use_origin() -> String { - #[cfg(target_arch = "wasm32")] { - use leptos::web_sys; - // Just get the current URL origin. - web_sys::window() - .unwrap() - .location() - .origin() - .unwrap_or("http://localhost:3000/".to_string()) - } - - #[cfg(not(target_arch = "wasm32"))] { - // Read the expected origin out of an environment variable. - std::env::var("LP_SITE_ORIGIN").unwrap_or("http://localhost:3000/".to_string()) - } -} - #[component] pub fn AppWithRoutes() -> impl IntoView { provide_meta_context(); - let current_origin = use_origin(); - - // Authentication setup - let parameters = AuthParameters { - issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM".into(), - client_id: "10jr2h3vtpu9n7gj46pvg5qo2q".into(), - redirect_uri: format!("{}/auth/callback", current_origin), - post_logout_redirect_uri: current_origin, - scope: Some("openid%20profile%20email".into()), - audience: None, - challenge: Challenge::None, - }; - - let auth = Auth::signal(); - provide_context(auth); // allows use of this signal in lower areas of the tree without - // explicitly passing it through the html tree - - let _ = Auth::init(parameters); - view! { // injects a stylesheet into the document // id=leptos means cargo-leptos will hot-reload this stylesheet - //

"Authentication is loading"

- // - // sets the document title @@ -99,27 +62,47 @@ pub fn AppWithRoutes() -> impl IntoView { // TODO Create a 404 page <Routes fallback=|| "Page not found.".into_view()> <Route path=path!("") view=HomePage /> - <Route path=path!("/auth/callback") view=AuthCallbackPage /> <Route path=path!("/about") view=AboutPage /> <Route path=path!("/test_page") view=TestPage /> <Route path=path!("/comparison") view=ComparisonTestPage /> - <Route path=path!("/providers") view=ProviderPortal /> - <Route path=path!("/providers/profile") view=ProviderContactPage /> - <ParentRoute path=path!("/providers/scholarships") view=ScholarshipInfoPage> - <Route path=path!(":id") view=ScholarshipInfoPage /> - <Route path=path!("") view=ScholarshipInfoPage /> - </ParentRoute> - <ParentRoute path=path!("loaners") view=LoanerShell> - <Route path=path!("") view=LoanerFallback /> - <Route path=path!("borrowing") view=LoanerBorrowForm /> - <Route path=path!("returning") view=LoanerReturnForm /> - </ParentRoute> + <ProviderRoutes /> + <LoanerRoutes /> </Routes> </ToastList> </main> } } +#[component(transparent)] +fn ProviderRoutes() -> impl MatchNestedRoutes + Clone { + view! { + <ParentRoute path=path!("/providers") view=ProviderLoginContext> + <Route path=path!("/callback") view=AuthCallbackPage /> + <Route path=path!("") view=ProviderPortal /> + <Route path=path!("/profile") view=ProviderContactPage /> + <ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage> + <Route path=path!(":id") view=ScholarshipInfoPage /> + <Route path=path!("") view=ScholarshipInfoPage /> + </ParentRoute> + </ParentRoute> + } + .into_inner() + .into_any_nested_route() +} + +#[component(transparent)] +fn LoanerRoutes() -> impl MatchNestedRoutes + Clone { + view! { + <ParentRoute path=path!("loaners") view=LoanerShell> + <Route path=path!("") view=LoanerFallback /> + <Route path=path!("borrowing") view=LoanerBorrowForm /> + <Route path=path!("returning") view=LoanerReturnForm /> + </ParentRoute> + } + .into_inner() + .into_any_nested_route() +} + #[component] pub fn AuthErrorView() -> impl IntoView { let auth = use_context::<AuthSignal>().expect("AuthError: RwSignal<AuthStore> was not found"); diff --git a/src/components/login/mod.rs b/src/components/login/mod.rs new file mode 100644 index 0000000..fae50eb --- /dev/null +++ b/src/components/login/mod.rs @@ -0,0 +1,5 @@ +mod provider; +pub use provider::*; +mod student; +pub use student::*; +pub(crate) mod setup; \ No newline at end of file diff --git a/src/components/login/provider.rs b/src/components/login/provider.rs new file mode 100644 index 0000000..ce1fb6d --- /dev/null +++ b/src/components/login/provider.rs @@ -0,0 +1,30 @@ +use leptos::prelude::*; +use leptos_router::components::Outlet; +use super::setup::provide_auth_context; + +///# Provider Login Context Component +/// +/// This component handles providing context to the provider login information. +/// It creates the corresponding [`AuthSignal`], specific to the student login page and redirects. +/// +/// Example usage: +/// ``` +/// view! { +/// <ProviderLoginContext> +/// // The rest of the student application. Don't forget AuthLoaded and +/// // Authenticated components! +/// </ProviderLoginContext> +/// } +#[component] +pub fn ProviderLoginContext() -> impl IntoView { + provide_auth_context( + crate::utils::use_origin(), + "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM", + "56c2bqvl021rv8d5mq36blt7jv", + "/providers/callback" + ); + + view! { + <Outlet /> + } +} diff --git a/src/components/login/setup.rs b/src/components/login/setup.rs new file mode 100644 index 0000000..a5ebae6 --- /dev/null +++ b/src/components/login/setup.rs @@ -0,0 +1,24 @@ +use leptos::prelude::*; +use leptos_oidc::{Auth, AuthParameters, Challenge}; + +pub fn provide_auth_context( + origin: String, + issuer: impl Into<String>, + client_id: impl Into<String>, + redirect_path: impl Into<String>, +) { + let params = AuthParameters { + issuer: issuer.into(), + client_id: client_id.into(), + redirect_uri: format!("{}{}", origin, redirect_path.into()), + post_logout_redirect_uri: origin, + scope: Some("openid%20profile%20email".into()), + audience: None, + challenge: Challenge::None, + }; + + let auth = Auth::signal(); + provide_context(auth); + + let _ = Auth::init(params); +} diff --git a/src/components/login/student.rs b/src/components/login/student.rs new file mode 100644 index 0000000..f3b89df --- /dev/null +++ b/src/components/login/student.rs @@ -0,0 +1,30 @@ +use leptos::prelude::*; +use leptos_router::components::Outlet; +use super::setup::provide_auth_context; + +///# Student Login Context Component +/// +/// This component handles providing context to the student login information. +/// It creates the corresponding [`AuthSignal`], specific to the student login page and redirects. +/// +/// Example usage: +/// ``` +/// view! { +/// <StudentLoginContext> +/// // The rest of the student application. Don't forget AuthLoaded and +/// // Authenticated components! +/// </StudentLoginContext> +/// } +#[component] +pub fn StudentLoginContext() -> impl IntoView { + provide_auth_context( + crate::utils::use_origin(), + "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM", + "10jr2h3vtpu9n7gj46pvg5qo2q", + "/students/callback" + ); + + view! { + <Outlet /> + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index ade0f49..27d6b69 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -12,6 +12,7 @@ mod validated_form; mod lists; mod toasts; mod header; +pub mod login; pub use self::{ action_button::*, banner::*, dashboard_button::*, lists::*, diff --git a/src/lib.rs b/src/lib.rs index 9958e9f..7ed69e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; mod common; mod components; mod pages; +mod utils; #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] diff --git a/src/pages/api/comparisons.rs b/src/pages/api/comparisons.rs index b60acef..bd00cd2 100644 --- a/src/pages/api/comparisons.rs +++ b/src/pages/api/comparisons.rs @@ -1,6 +1,5 @@ -use std::collections::HashMap; #[cfg(feature = "ssr")] -use crate::pages::utils::server_utils::create_dynamo_client; +use crate::utils::server::create_dynamo_client; #[cfg(feature = "ssr")] use aws_sdk_dynamodb::error::ProvideErrorMetadata; @@ -12,6 +11,7 @@ use crate::common::{ ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison, TextComparison, TextListComparison, ValueType, }; +use std::collections::HashMap; use leptos::prelude::ServerFnError; use leptos::server; diff --git a/src/pages/api/scholarships.rs b/src/pages/api/scholarships.rs index aaf5d47..d4071d0 100644 --- a/src/pages/api/scholarships.rs +++ b/src/pages/api/scholarships.rs @@ -8,7 +8,7 @@ use leptos::leptos_dom::log; #[cfg(feature = "ssr")] use uuid::Uuid; #[cfg(feature = "ssr")] -use crate::pages::utils::server_utils::create_dynamo_client; +use crate::utils::server::{create_dynamo_client, into_attr_map}; #[cfg(feature = "ssr")] use crate::common::ValueType; @@ -51,7 +51,6 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn #[server(CreateScholarshipInfo, endpoint = "/scholarship/info/create")] pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> { - use crate::pages::utils::server_utils::create_dynamo_client; use aws_sdk_dynamodb::error::ProvideErrorMetadata; let client = create_dynamo_client().await; diff --git a/src/pages/comparison_test.rs b/src/pages/comparison_test.rs index 57afa5a..50dd46f 100644 --- a/src/pages/comparison_test.rs +++ b/src/pages/comparison_test.rs @@ -3,12 +3,12 @@ use super::api::{get_all_scholarship_info, get_comparison_info}; use super::home_page::get_submission; use crate::common::{ComparisonData, ExpandableInfo, ValueType}; use crate::components::{ActionButton, Loading, Panel, Row}; -use crate::pages::utils::get_user_claims; use leptos::either::Either; use leptos::leptos_dom::logging::console_log; use leptos::logging::log; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; +use crate::utils::get_user_claims; #[component] pub fn ComparisonTestPage() -> impl IntoView { diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index f303195..4e109a8 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -10,7 +10,6 @@ use std::io::Write; use crate::common::{ComparisonData, ExpandableInfo}; use crate::pages::UnauthenticatedPage; -use crate::pages::utils::get_user_claims; use crate::components::{ ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm @@ -26,6 +25,7 @@ use leptos::web_sys::HtmlAnchorElement; use leptos::wasm_bindgen::JsCast; use std::collections::HashMap; use crate::pages::api::get_comparisons_categorized; +use crate::utils::get_user_claims; /// # Get Student Info /// Gets a student's information given their `subject`. diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 1b363d8..6b76373 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -7,7 +7,6 @@ mod provider_portal; mod scholarship_info; mod test_page; mod unauthenticated_page; -mod utils; mod provider_contact; mod auth_callback; diff --git a/src/pages/provider_contact.rs b/src/pages/provider_contact.rs index 55a01b4..ea03899 100644 --- a/src/pages/provider_contact.rs +++ b/src/pages/provider_contact.rs @@ -1,14 +1,14 @@ -use std::collections::HashMap; +#[cfg(feature = "ssr")] +use crate::utils::server::{create_dynamo_client, into_attr_map}; + +use std::collections::HashMap; use leptos::logging::log; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; use crate::common::ValueType; use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm}; use crate::pages::UnauthenticatedPage; -use crate::pages::utils::get_user_claims; - -#[cfg(feature = "ssr")] -use crate::pages::utils::server_utils::create_dynamo_client; +use crate::utils::get_user_claims; #[cfg(feature = "ssr")] static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts"; @@ -52,7 +52,6 @@ async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, #[server] async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> { use aws_sdk_dynamodb::error::ProvideErrorMetadata; - use crate::pages::utils::server_utils::into_attr_map; let client = create_dynamo_client().await; diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index df41afd..53bca79 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -8,7 +8,7 @@ use leptos_router::hooks::{use_navigate, use_params}; use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType}; use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm}; use super::UnauthenticatedPage; -use crate::pages::utils::get_user_claims; +use crate::utils::get_user_claims; use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized, CreateTestComparisons}; diff --git a/src/pages/utils.rs b/src/pages/utils.rs deleted file mode 100644 index cc26488..0000000 --- a/src/pages/utils.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::common::UserClaims; -use leptos::prelude::{Signal, With, use_context}; -use leptos_oidc::{Algorithm, AuthSignal, TokenData}; - -/// # Server Utilities -/// -/// These utility functions can only be used from the server. They are created to use server-side -/// dependencies like the AWS SDK. Use of these functions will crash the front end, as these -/// dependencies cannot be compiled for WASM (nor should they be). -#[cfg(feature = "ssr")] -pub mod server_utils { - use std::collections::HashMap; - use aws_config::SdkConfig; - use crate::common::ValueType; - - /// Creates an [`SdkConfig`] struct for use with AWS SDK structs. - pub async fn create_aws_config() -> SdkConfig { - aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await - } - - /// Creates a DynamoDB client. - pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client { - aws_sdk_dynamodb::Client::new(&create_aws_config().await) - } - - pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> { - map.into_iter().map(|(k, v)| (k, v.into())).collect() - } -} - -/// Gets the current user claims. This function should only be used in an area that has -/// access to an AuthSignal, or it will result in a total failure. -pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> { - let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal."); - Signal::derive(move || { - auth.with(|auth| { - auth.authenticated().and_then(|data| { - data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"]) - }) - }) - }) -} diff --git a/src/utils/common.rs b/src/utils/common.rs new file mode 100644 index 0000000..a80584d --- /dev/null +++ b/src/utils/common.rs @@ -0,0 +1,33 @@ +use leptos::prelude::*; +use leptos_oidc::{Algorithm, AuthSignal, TokenData}; +use crate::common::UserClaims; + +pub fn use_origin() -> String { + #[cfg(target_arch = "wasm32")] { + use leptos::web_sys; + // Just get the current URL origin. + web_sys::window() + .unwrap() + .location() + .origin() + .unwrap_or("http://localhost:3000/".to_string()) + } + + #[cfg(not(target_arch = "wasm32"))] { + // Read the expected origin out of an environment variable. + std::env::var("LP_SITE_ORIGIN").unwrap_or("http://localhost:3000/".to_string()) + } +} + +/// Gets the current user claims. This function should only be used in an area that has +/// access to an AuthSignal, or it will result in a total failure. +pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> { + let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal."); + Signal::derive(move || { + auth.with(|auth| { + auth.authenticated().and_then(|data| { + data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"]) + }) + }) + }) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..93fe774 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "ssr")] +pub mod server; + +mod common; +pub use common::*; \ No newline at end of file diff --git a/src/utils/server.rs b/src/utils/server.rs new file mode 100644 index 0000000..e6c5c9b --- /dev/null +++ b/src/utils/server.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; +use aws_config::SdkConfig; +use crate::common::ValueType; + +/// Creates an [`SdkConfig`] struct for use with AWS SDK structs. +pub async fn create_aws_config() -> SdkConfig { + aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await +} + +/// Creates a DynamoDB client. +pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client { + aws_sdk_dynamodb::Client::new(&create_aws_config().await) +} + +pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> { + map.into_iter().map(|(k, v)| (k, v.into())).collect() +} \ No newline at end of file