From 0f20ba3b8607278a56d56252252fa1a68a53aec1 Mon Sep 17 00:00:00 2001 From: Matthew Kaminski Date: Mon, 26 Aug 2024 00:09:08 -0400 Subject: [PATCH] Added basic logging in Needs a lot of work --- Cargo.toml | 2 +- src/capsules/login_form.rs | 69 +++++++++++++++++++++++++++++----- src/components/header.rs | 8 ++-- src/endpoints.rs | 4 +- src/main.rs | 1 + src/models/auth.rs | 23 ++++++++++++ src/models/mod.rs | 1 + src/server/auth/login.rs | 63 +++++++++++++++++++++++++++++++ src/server/auth/mod.rs | 1 + src/server/mod.rs | 1 + src/server/routes.rs | 27 ++++--------- src/templates/add_game_form.rs | 5 +-- src/templates/global_state.rs | 14 ++++--- 13 files changed, 174 insertions(+), 45 deletions(-) create mode 100644 src/models/auth.rs create mode 100644 src/models/mod.rs create mode 100644 src/server/auth/login.rs create mode 100644 src/server/auth/mod.rs diff --git a/Cargo.toml b/Cargo.toml index ed3d193..796ec1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ cfg-if = "1.0.0" chrono = { version = "0.4.38", features = ["serde", "wasm-bindgen"] } password-auth = "1.0.0" lazy_static = "1.5" - +jsonwebtoken = "9" [target.'cfg(engine)'.dev-dependencies] fantoccini = "0.19" diff --git a/src/capsules/login_form.rs b/src/capsules/login_form.rs index 5f765fe..fd4d121 100644 --- a/src/capsules/login_form.rs +++ b/src/capsules/login_form.rs @@ -1,10 +1,25 @@ +use std::num::NonZeroU16; + use lazy_static::lazy_static; use perseus::prelude::*; use serde::{Deserialize, Serialize}; use sycamore::prelude::*; use web_sys::Event; -use crate::{state_enums::OpenState, templates::global_state::AppStateRx}; +use crate::{ + endpoints::LOGIN, + state_enums::OpenState, + templates::{get_api_path, global_state::AppStateRx}, +}; + +cfg_if::cfg_if! { + if #[cfg(client)] { + use crate::{ + models::auth::{LoginInfo, LoginResponse}, + }; + use reqwest::StatusCode; + } +} lazy_static! { pub static ref LOGIN_FORM: Capsule = get_capsule(); @@ -15,6 +30,7 @@ lazy_static! { struct LoginFormState { username: String, password: String, + remember_me: bool, } #[derive(Clone)] @@ -41,6 +57,38 @@ fn login_form_capsule( } }; + let handle_log_in = move |_event: Event| { + #[cfg(client)] + { + spawn_local_scoped(cx, async move { + let login_info = LoginInfo { + username: state.username.get().as_ref().clone(), + password: state.password.get().as_ref().clone(), + remember_me: state.remember_me.get().as_ref().clone(), + }; + + // // @todo clean up error handling + let client = reqwest::Client::new(); + let response = client + .post(get_api_path(LOGIN).as_str()) + .json(&login_info) + .send() + .await + .unwrap(); + + let global_state = Reactor::::from_cx(cx).get_global_state::(cx); + + if response.status() != StatusCode::OK { + state.username.set(response.status().to_string()); + return; + } + + let response = response.json::().await.unwrap(); + state.username.set(response.token.clone()); + }); + } + }; + view! { cx, div (class="overflow-x-hidden overflow-y-auto fixed h-modal md:h-full top-4 left-0 right-0 md:inset-0 z-50 justify-center items-center"){ div (class="relative md:mx-auto w-full md:w-1/2 lg:w-1/3 z-0 my-10") { @@ -50,20 +98,20 @@ fn login_form_capsule( "Back" } } - form (class="space-y-6 px-6 lg:px-8 pb-4 sm:pb-6 xl:pb-8") { + div (class="space-y-6 px-6 lg:px-8 pb-4 sm:pb-6 xl:pb-8") { h3 (class="text-xl font-medium text-gray-900 dark:text-white"){"Sign in to our platform"} div { - label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300") {"Your email"} - input (class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white") {} + label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300") {"Username"} + input (bind:value = state.username, class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white") {} } div { - label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300"){"Your password"} - input (class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"){} + label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300"){"Password"} + input (bind:value = state.password, class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"){} } div (class="flex justify-between"){ div (class="flex items-start"){ div (class="flex items-center h-5"){ - input (class="bg-gray-50 border border-gray-300 focus:ring-3 focus:ring-blue-300 h-4 w-4 rounded dark:bg-gray-600 dark:border-gray-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800") {} + input (bind:checked = state.remember_me, type = "checkbox", class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600") {} } div (class="text-sm ml-3"){ label (class="font-medium text-gray-900 dark:text-gray-300"){"Remember me"} @@ -71,7 +119,7 @@ fn login_form_capsule( } a (class="text-sm text-blue-700 hover:underline dark:text-blue-500"){"Lost Password?"} } - button (class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"){"Login to your account"} + button (on:click = handle_log_in, class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"){"Log in"} div (class="text-sm font-medium text-gray-500 dark:text-gray-300"){ a (class="text-blue-700 hover:underline dark:text-blue-500"){"Create account"} } @@ -92,7 +140,8 @@ pub fn get_capsule() -> Capsule { #[engine_only_fn] async fn get_build_state(_info: StateGeneratorInfo<()>) -> LoginFormState { LoginFormState { - username: "".to_string(), - password: "".to_string(), + username: "".to_owned(), + password: "".to_owned(), + remember_me: false, } } diff --git a/src/components/header.rs b/src/components/header.rs index 3b495ae..7fcf713 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -6,6 +6,8 @@ use web_sys::Event; use crate::{ capsules::login_form::{LoginFormProps, LOGIN_FORM}, + endpoints::LOGIN, + models::auth::LoginInfo, state_enums::{GameState, LoginState, OpenState}, templates::global_state::AppStateRx, }; @@ -81,9 +83,9 @@ pub fn Header<'a, G: Html>(cx: Scope<'a>, HeaderProps { game, title }: HeaderPro (LOGIN_FORM.widget(cx, "", LoginFormProps{ remember_me: true, - endpoint: "".to_string(), - lost_password_url: Some("".to_string()), - forgot_password_url: Some("".to_string()), + endpoint: "".to_owned(), + lost_password_url: Some("".to_owned()), + forgot_password_url: Some("".to_owned()), } )) } diff --git a/src/endpoints.rs b/src/endpoints.rs index 139ed38..33ed856 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -1,2 +1,2 @@ -pub const MATCH: &str = "/api/post-match"; -pub const USER: &str = "/api/post-user"; +pub const LOGIN: &str = "/api/login"; +pub const LOGIN_TEST: &str = "/api/login-test"; diff --git a/src/main.rs b/src/main.rs index e37f41d..e762c56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod endpoints; #[allow(unused_imports)] mod entity; mod error_views; +mod models; #[cfg(engine)] mod server; mod state_enums; diff --git a/src/models/auth.rs b/src/models/auth.rs new file mode 100644 index 0000000..6ca5771 --- /dev/null +++ b/src/models/auth.rs @@ -0,0 +1,23 @@ +use chrono::serde::ts_seconds; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct LoginInfo { + pub username: String, + pub password: String, + pub remember_me: bool, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct LoginResponse { + pub token: String, + #[serde(with = "ts_seconds")] + pub expires: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Claims { + pub sub: String, + pub exp: usize, +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..0e4a05d --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/src/server/auth/login.rs b/src/server/auth/login.rs new file mode 100644 index 0000000..aec843e --- /dev/null +++ b/src/server/auth/login.rs @@ -0,0 +1,63 @@ +use crate::models::auth::{Claims, LoginInfo, LoginResponse}; +use axum::{ + extract::Json, + http::{HeaderMap, StatusCode}, +}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; + +pub fn is_valid_user(username: &str, password: &str) -> bool { + return true; +} + +pub async fn post_login_user( + Json(login_info): Json, +) -> Result, StatusCode> { + let user_authenticated = is_valid_user(&login_info.username, &login_info.password); + + match user_authenticated { + false => Err(StatusCode::UNAUTHORIZED), + true => { + let expires = match login_info.remember_me { + true => chrono::Utc::now() + chrono::Duration::days(365), + false => chrono::Utc::now() + chrono::Duration::days(1), + }; + + let claims = Claims { + sub: login_info.username.clone(), + exp: expires.timestamp() as usize, + }; + // @todo change secret + let token = match encode( + &Header::default(), + &claims, + &EncodingKey::from_secret("secret".as_ref()), + ) { + Ok(token) => token, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + + let resp = LoginResponse { token, expires }; + Ok(Json(resp)) + } + } +} + +pub async fn post_test_login(header_map: HeaderMap) -> Result, StatusCode> { + if let Some(auth_header) = header_map.get("Authorization") { + if let Ok(auth_header_str) = auth_header.to_str() { + if auth_header_str.starts_with("Bearer ") { + let token = auth_header_str.trim_start_matches("Bearer ").to_string(); + // @todo change secret + match decode::( + &token, + &DecodingKey::from_secret("secret".as_ref()), + &Validation::default(), + ) { + Ok(_) => return Ok(Json("Logged in".to_owned())), + Err(_) => {} + } + } + } + } + Err(StatusCode::UNAUTHORIZED) +} diff --git a/src/server/auth/mod.rs b/src/server/auth/mod.rs new file mode 100644 index 0000000..320cbbb --- /dev/null +++ b/src/server/auth/mod.rs @@ -0,0 +1 @@ +pub mod login; diff --git a/src/server/mod.rs b/src/server/mod.rs index 6a664ab..474f258 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1 +1,2 @@ +pub mod auth; pub mod routes; diff --git a/src/server/routes.rs b/src/server/routes.rs index b65cb22..2fbccdc 100644 --- a/src/server/routes.rs +++ b/src/server/routes.rs @@ -1,24 +1,11 @@ // (Server only) Routes -use crate::{ - endpoints::{MATCH, USER}, - entity::{game, user}, -}; -use axum::{ - extract::Json, - routing::{post, Router}, -}; +use crate::endpoints::{LOGIN, LOGIN_TEST}; +use axum::routing::{post, Router}; + +use super::auth::login::{post_login_user, post_test_login}; pub fn register_routes(app: Router) -> Router { - let app = app.route(USER, post(post_user)); - app.route(MATCH, post(post_match)) -} - -async fn post_user(_user: String) -> Json { - // Update the store with the new match - todo!() -} - -async fn post_match(_user: String) -> Json { - // Update the store with the new match - todo!() + let app = app.route(LOGIN, post(post_login_user)); + let app = app.route(LOGIN_TEST, post(post_test_login)); + app } diff --git a/src/templates/add_game_form.rs b/src/templates/add_game_form.rs index a02d1a1..da566c3 100644 --- a/src/templates/add_game_form.rs +++ b/src/templates/add_game_form.rs @@ -7,7 +7,6 @@ use web_sys::Event; cfg_if::cfg_if! { if #[cfg(client)] { use crate::templates::global_state::AppStateRx; - use crate::endpoints::{MATCH, USER}; use crate::templates::get_api_path; use chrono::Utc; } @@ -85,8 +84,8 @@ async fn get_request_state( _req: Request, ) -> Result> { Ok(PageState { - winner: "Ferris".to_string(), - new_user: "newguy".to_string(), + winner: "Ferris".to_owned(), + new_user: "newguy".to_owned(), }) } diff --git a/src/templates/global_state.rs b/src/templates/global_state.rs index b96d6ec..fb2190c 100644 --- a/src/templates/global_state.rs +++ b/src/templates/global_state.rs @@ -3,7 +3,10 @@ use perseus::{prelude::*, state::GlobalStateCreator}; use serde::{Deserialize, Serialize}; -use crate::state_enums::{LoginState, OpenState}; +use crate::{ + models::auth::Claims, + state_enums::{LoginState, OpenState}, +}; cfg_if::cfg_if! { if #[cfg(engine)] { @@ -34,10 +37,6 @@ pub struct ModalOpenData { pub login: OpenState, } -#[derive(Serialize, Deserialize, ReactiveState, Clone)] -#[rx(alias = "ClaimsRx")] -pub struct Claims {} - pub fn get_global_state_creator() -> GlobalStateCreator { GlobalStateCreator::new().build_state_fn(get_build_state) } @@ -48,7 +47,10 @@ pub async fn get_build_state() -> AppState { auth: AuthData { state: LoginState::Unknown, username: None, - claims: Claims {}, + claims: Claims { + sub: "".to_owned(), + exp: 0, + }, }, modals_open: ModalOpenData { login: OpenState::Closed,