diff --git a/migration/src/m20240813_000001_create_users.rs b/migration/src/m20240813_000001_create_users.rs index dbfe201..1c6dcd2 100644 --- a/migration/src/m20240813_000001_create_users.rs +++ b/migration/src/m20240813_000001_create_users.rs @@ -9,6 +9,7 @@ pub struct Migration; impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { // User table + // @todo verify all data saved is length-checked manager .create_table( Table::create() @@ -22,6 +23,7 @@ impl MigrationTrait for Migration { .col(boolean(User::IsAdmin)) .col(string_null(User::Email)) .col(string_null(User::Avatar)) + .col(string_null(User::ForgotPasswordRequest)) .to_owned(), ) .await @@ -46,4 +48,5 @@ pub enum User { IsAdmin, Email, Avatar, + ForgotPasswordRequest, } diff --git a/src/capsules/forgot_password_form.rs b/src/capsules/forgot_password_form.rs index fa7c7cf..5efb6c8 100644 --- a/src/capsules/forgot_password_form.rs +++ b/src/capsules/forgot_password_form.rs @@ -49,6 +49,8 @@ fn forgot_password_form_capsule( { spawn_local_scoped(cx, async move { let global_state = Reactor::::from_cx(cx).get_global_state::(cx); + + // Close modal state.reset(); global_state .modals_open @@ -83,20 +85,17 @@ fn forgot_password_form_capsule( } } 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"} + h3 (class="text-xl font-medium text-gray-900 dark:text-white"){"Forgot Password"} div { 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"){"How to contact you with new password"} + label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300"){"Contact Info"} input (bind:value = state.how_to_reach, 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"){} } - button (on:click = handle_submit, 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"){"Submit"} - } + button (on:click = handle_submit, 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"){"Submit"} } } } diff --git a/src/capsules/login_form.rs b/src/capsules/login_form.rs index 414b4de..b0ab949 100644 --- a/src/capsules/login_form.rs +++ b/src/capsules/login_form.rs @@ -76,6 +76,19 @@ fn login_form_capsule( } }; + let handle_register = move |_event: Event| { + #[cfg(client)] + { + spawn_local_scoped(cx, async move { + let global_state = Reactor::::from_cx(cx).get_global_state::(cx); + global_state.modals_open.register.set(OpenState::Open); + // Close modal + state.reset(); + global_state.modals_open.login.set(OpenState::Closed); + }); + } + }; + let handle_log_in = move |_event: Event| { #[cfg(client)] { @@ -155,11 +168,11 @@ fn login_form_capsule( }}, false => view!{cx, }, }) - a (on:click = handle_forgot_password, class="text-sm text-blue-700 hover:underline dark:text-blue-500"){"Lost Password?"} + button (on:click = handle_forgot_password, class="text-sm text-blue-700 hover:underline dark:text-blue-500"){"Lost Password?"} } 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"} + button (on:click = handle_register, class="text-blue-700 hover:underline dark:text-blue-500"){"Create account"} } } } diff --git a/src/capsules/register_form.rs b/src/capsules/register_form.rs index e69de29..8b13789 100644 --- a/src/capsules/register_form.rs +++ b/src/capsules/register_form.rs @@ -0,0 +1 @@ + diff --git a/src/components/header.rs b/src/components/header.rs index cb862bc..4d12dc4 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -92,34 +92,5 @@ pub fn Header<'a, G: Html>(cx: Scope<'a>, HeaderProps { game, title }: HeaderPro } } } - - section(class = "flex-2") { - (match *global_state.modals_open.login.get() { - OpenState::Open => { - view! { cx, - (LOGIN_FORM.widget(cx, "", - LoginFormProps{ - remember_me: true, - } - )) - } - } - OpenState::Closed => { - view!{ cx, } - } - }) - (match *global_state.modals_open.forgot_password.get() { - OpenState::Open => { - view! { cx, - (FORGOT_PASSWORD_FORM.widget(cx, "", - ForgotPasswordFormProps{} - )) - } - } - OpenState::Closed => { - view!{ cx, } - } - }) - } } } diff --git a/src/components/layout.rs b/src/components/layout.rs index e9bcc36..4870289 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -1,8 +1,11 @@ use crate::{ - capsules::login_form::{LoginFormProps, LOGIN_FORM}, + capsules::{ + forgot_password_form::{ForgotPasswordFormProps, FORGOT_PASSWORD_FORM}, + login_form::{LoginFormProps, LOGIN_FORM}, + }, components::header::{Header, HeaderProps}, global_state::AppStateRx, - state_enums::{GameState, LoginState}, + state_enums::{GameState, LoginState, OpenState}, }; use perseus::prelude::*; use sycamore::prelude::*; @@ -28,8 +31,6 @@ pub fn Layout<'a, G: Html>( ) -> View { let children = children.call(cx); - // Get global state to get authentication info - #[cfg(client)] let global_state = Reactor::::from_cx(cx).get_global_state::(cx); // Check if the client is authenticated or not @@ -40,6 +41,36 @@ pub fn Layout<'a, G: Html>( // Main page header, including login functionality Header(game = game, title = title) + // Modals + section(class = "flex-2") { + (match *global_state.modals_open.login.get() { + OpenState::Open => { + view! { cx, + (LOGIN_FORM.widget(cx, "", + LoginFormProps{ + remember_me: true, + } + )) + } + } + OpenState::Closed => { + view!{ cx, } + } + }) + (match *global_state.modals_open.forgot_password.get() { + OpenState::Open => { + view! { cx, + (FORGOT_PASSWORD_FORM.widget(cx, "", + ForgotPasswordFormProps{} + )) + } + } + OpenState::Closed => { + view!{ cx, } + } + }) + } + main(style = "my-8") { // Body header div { @@ -59,7 +90,7 @@ pub fn Layout<'a, G: Html>( } } } - // Actual body + // Content body div(class = "container mx-auto px-6") { div(class = "md:flex mt-8 md:-mx-4") { div(class = "rounded-md overflow-hidden bg-cover bg-center") { diff --git a/src/endpoints.rs b/src/endpoints.rs index 33ed856..87f1288 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -1,2 +1,4 @@ +pub const REGISTER: &str = "/api/login"; pub const LOGIN: &str = "/api/login"; pub const LOGIN_TEST: &str = "/api/login-test"; +pub const FORGOT_PASSWORD: &str = "/api/forgot-password"; diff --git a/src/entity/user.rs b/src/entity/user.rs index 82cc0ed..6bb30a3 100644 --- a/src/entity/user.rs +++ b/src/entity/user.rs @@ -16,6 +16,7 @@ pub struct Model { pub is_admin: bool, pub email: Option, pub avatar: Option, + pub forgot_password_request: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/src/global_state.rs b/src/global_state.rs index f25fb3e..98c0472 100644 --- a/src/global_state.rs +++ b/src/global_state.rs @@ -81,6 +81,7 @@ impl AuthDataRx { #[rx(alias = "ModalOpenDataRx")] pub struct ModalOpenData { pub login: OpenState, + pub register: OpenState, pub forgot_password: OpenState, } @@ -99,6 +100,7 @@ pub async fn get_build_state() -> AppState { }, modals_open: ModalOpenData { login: OpenState::Closed, + register: OpenState::Closed, forgot_password: OpenState::Closed, }, } diff --git a/src/main.rs b/src/main.rs index 39df0d3..6c81df4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,9 +23,10 @@ cfg_if::cfg_if! { stores::MutableStore, turbine::Turbine, }; + use crate::server::routes::get_api_router; + use crate::server::server_state::ServerState; use futures::executor::block_on; - use sea_orm::{Database}; - use crate::server::routes::register_routes; + use sea_orm::Database; } } @@ -38,16 +39,24 @@ pub async fn dflt_server Update to use environment variable - if let Err(err) = block_on(Database::connect( - "postgres://elo:elo@localhost:5432/elo_app", - )) { - panic!("{}", err); - } + // TODO -> error handling + // Includes making database connection + let db_conn = Database::connect("postgres://elo:elo@localhost:5432/elo_app"); + let db_conn = block_on(db_conn); + let db_conn = match db_conn { + Ok(db_conn) => db_conn, + Err(err) => { + panic!("{}", err); + } + }; + let state = ServerState { db_conn }; + + // Get server routes + let api_router = get_api_router(state); + let app = app.merge(api_router); axum::Server::bind(&addr) .serve(app.into_make_service()) diff --git a/src/models/auth.rs b/src/models/auth.rs index 66ca6b0..cc7349f 100644 --- a/src/models/auth.rs +++ b/src/models/auth.rs @@ -31,3 +31,9 @@ pub struct WebAuthInfo { pub username: String, pub remember_me: bool, } + +#[derive(Serialize, Deserialize, Clone)] +pub struct ForgotPasswordRequest { + pub username: String, + pub contact_info: String, +} diff --git a/src/server/auth/forgot_password.rs b/src/server/auth/forgot_password.rs new file mode 100644 index 0000000..0df2656 --- /dev/null +++ b/src/server/auth/forgot_password.rs @@ -0,0 +1,13 @@ +use crate::{models::auth::ForgotPasswordRequest, server::server_state::ServerState}; +use axum::{ + extract::{Json, State}, + http::{HeaderMap, StatusCode}, +}; +use sea_orm::DatabaseConnection; + +pub async fn post_forgot_password( + State(state): State, + Json(password_request): Json, +) -> StatusCode { + StatusCode::OK +} diff --git a/src/server/auth/login.rs b/src/server/auth/login.rs index aec843e..c687a47 100644 --- a/src/server/auth/login.rs +++ b/src/server/auth/login.rs @@ -1,6 +1,9 @@ -use crate::models::auth::{Claims, LoginInfo, LoginResponse}; +use crate::{ + models::auth::{Claims, LoginInfo, LoginResponse}, + server::server_state::ServerState, +}; use axum::{ - extract::Json, + extract::{Json, State}, http::{HeaderMap, StatusCode}, }; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; @@ -10,6 +13,7 @@ pub fn is_valid_user(username: &str, password: &str) -> bool { } pub async fn post_login_user( + State(state): State, Json(login_info): Json, ) -> Result, StatusCode> { let user_authenticated = is_valid_user(&login_info.username, &login_info.password); @@ -42,7 +46,10 @@ pub async fn post_login_user( } } -pub async fn post_test_login(header_map: HeaderMap) -> Result, StatusCode> { +pub async fn post_test_login( + State(state): State, + 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 ") { diff --git a/src/server/auth/mod.rs b/src/server/auth/mod.rs index 320cbbb..85b2611 100644 --- a/src/server/auth/mod.rs +++ b/src/server/auth/mod.rs @@ -1 +1,2 @@ +pub mod forgot_password; pub mod login; diff --git a/src/server/mod.rs b/src/server/mod.rs index 474f258..a3505e9 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,2 +1,3 @@ pub mod auth; pub mod routes; +pub mod server_state; diff --git a/src/server/routes.rs b/src/server/routes.rs index 2fbccdc..baf4a77 100644 --- a/src/server/routes.rs +++ b/src/server/routes.rs @@ -1,11 +1,21 @@ // (Server only) Routes -use crate::endpoints::{LOGIN, LOGIN_TEST}; +use crate::endpoints::{FORGOT_PASSWORD, LOGIN, LOGIN_TEST}; use axum::routing::{post, Router}; +use futures::executor::block_on; +use sea_orm::Database; -use super::auth::login::{post_login_user, post_test_login}; +use super::{ + auth::{ + forgot_password::post_forgot_password, + login::{post_login_user, post_test_login}, + }, + server_state::ServerState, +}; -pub fn register_routes(app: Router) -> Router { - let app = app.route(LOGIN, post(post_login_user)); - let app = app.route(LOGIN_TEST, post(post_test_login)); - app +pub fn get_api_router(state: ServerState) -> Router { + Router::new() + .route(LOGIN, post(post_login_user)) + .route(LOGIN_TEST, post(post_test_login)) + .route(FORGOT_PASSWORD, post(post_forgot_password)) + .with_state(state) } diff --git a/src/server/server_state.rs b/src/server/server_state.rs new file mode 100644 index 0000000..b4fbed2 --- /dev/null +++ b/src/server/server_state.rs @@ -0,0 +1,6 @@ +use sea_orm::DatabaseConnection; + +#[derive(Clone)] +pub struct ServerState { + pub db_conn: DatabaseConnection, +}