Add database to server endpoints, move modals, add forget pw to db
All checks were successful
Build Crate / build (push) Successful in 1m45s

This commit is contained in:
2024-08-27 02:12:57 -04:00
parent 242f9b1218
commit f4f491085d
17 changed files with 137 additions and 61 deletions

View File

@@ -9,6 +9,7 @@ pub struct Migration;
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// User table // User table
// @todo verify all data saved is length-checked
manager manager
.create_table( .create_table(
Table::create() Table::create()
@@ -22,6 +23,7 @@ impl MigrationTrait for Migration {
.col(boolean(User::IsAdmin)) .col(boolean(User::IsAdmin))
.col(string_null(User::Email)) .col(string_null(User::Email))
.col(string_null(User::Avatar)) .col(string_null(User::Avatar))
.col(string_null(User::ForgotPasswordRequest))
.to_owned(), .to_owned(),
) )
.await .await
@@ -46,4 +48,5 @@ pub enum User {
IsAdmin, IsAdmin,
Email, Email,
Avatar, Avatar,
ForgotPasswordRequest,
} }

View File

@@ -49,6 +49,8 @@ fn forgot_password_form_capsule<G: Html>(
{ {
spawn_local_scoped(cx, async move { spawn_local_scoped(cx, async move {
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx); let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
// Close modal
state.reset(); state.reset();
global_state global_state
.modals_open .modals_open
@@ -83,20 +85,17 @@ fn forgot_password_form_capsule<G: Html>(
} }
} }
div (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"} h3 (class="text-xl font-medium text-gray-900 dark:text-white"){"Forgot Password"}
div { div {
label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300") {"Username"} 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") {} 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 { 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"){} 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"} 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"}
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"}
}
} }
} }
} }

View File

@@ -76,6 +76,19 @@ fn login_form_capsule<G: Html>(
} }
}; };
let handle_register = move |_event: Event| {
#[cfg(client)]
{
spawn_local_scoped(cx, async move {
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(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| { let handle_log_in = move |_event: Event| {
#[cfg(client)] #[cfg(client)]
{ {
@@ -155,11 +168,11 @@ fn login_form_capsule<G: Html>(
}}, }},
false => view!{cx, }, 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"} 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"){ 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"}
} }
} }
} }

View File

@@ -0,0 +1 @@

View File

@@ -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, }
}
})
}
} }
} }

View File

@@ -1,8 +1,11 @@
use crate::{ 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}, components::header::{Header, HeaderProps},
global_state::AppStateRx, global_state::AppStateRx,
state_enums::{GameState, LoginState}, state_enums::{GameState, LoginState, OpenState},
}; };
use perseus::prelude::*; use perseus::prelude::*;
use sycamore::prelude::*; use sycamore::prelude::*;
@@ -28,8 +31,6 @@ pub fn Layout<'a, G: Html>(
) -> View<G> { ) -> View<G> {
let children = children.call(cx); let children = children.call(cx);
// Get global state to get authentication info
#[cfg(client)]
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx); let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
// Check if the client is authenticated or not // Check if the client is authenticated or not
@@ -40,6 +41,36 @@ pub fn Layout<'a, G: Html>(
// Main page header, including login functionality // Main page header, including login functionality
Header(game = game, title = title) 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") { main(style = "my-8") {
// Body header // Body header
div { div {
@@ -59,7 +90,7 @@ pub fn Layout<'a, G: Html>(
} }
} }
} }
// Actual body // Content body
div(class = "container mx-auto px-6") { div(class = "container mx-auto px-6") {
div(class = "md:flex mt-8 md:-mx-4") { div(class = "md:flex mt-8 md:-mx-4") {
div(class = "rounded-md overflow-hidden bg-cover bg-center") { div(class = "rounded-md overflow-hidden bg-cover bg-center") {

View File

@@ -1,2 +1,4 @@
pub const REGISTER: &str = "/api/login";
pub const LOGIN: &str = "/api/login"; pub const LOGIN: &str = "/api/login";
pub const LOGIN_TEST: &str = "/api/login-test"; pub const LOGIN_TEST: &str = "/api/login-test";
pub const FORGOT_PASSWORD: &str = "/api/forgot-password";

View File

@@ -16,6 +16,7 @@ pub struct Model {
pub is_admin: bool, pub is_admin: bool,
pub email: Option<String>, pub email: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub forgot_password_request: Option<String>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -81,6 +81,7 @@ impl AuthDataRx {
#[rx(alias = "ModalOpenDataRx")] #[rx(alias = "ModalOpenDataRx")]
pub struct ModalOpenData { pub struct ModalOpenData {
pub login: OpenState, pub login: OpenState,
pub register: OpenState,
pub forgot_password: OpenState, pub forgot_password: OpenState,
} }
@@ -99,6 +100,7 @@ pub async fn get_build_state() -> AppState {
}, },
modals_open: ModalOpenData { modals_open: ModalOpenData {
login: OpenState::Closed, login: OpenState::Closed,
register: OpenState::Closed,
forgot_password: OpenState::Closed, forgot_password: OpenState::Closed,
}, },
} }

View File

@@ -23,9 +23,10 @@ cfg_if::cfg_if! {
stores::MutableStore, stores::MutableStore,
turbine::Turbine, turbine::Turbine,
}; };
use crate::server::routes::get_api_router;
use crate::server::server_state::ServerState;
use futures::executor::block_on; use futures::executor::block_on;
use sea_orm::{Database}; use sea_orm::Database;
use crate::server::routes::register_routes;
} }
} }
@@ -38,16 +39,24 @@ pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'st
let addr: SocketAddr = format!("{}:{}", host, port) let addr: SocketAddr = format!("{}:{}", host, port)
.parse() .parse()
.expect("Invalid address provided to bind to."); .expect("Invalid address provided to bind to.");
let mut app = perseus_axum::get_router(turbine, opts).await; let app = perseus_axum::get_router(turbine, opts).await;
app = register_routes(app);
// TODO -> Update to use environment variable // TODO -> Update to use environment variable
if let Err(err) = block_on(Database::connect( // TODO -> error handling
"postgres://elo:elo@localhost:5432/elo_app", // 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); 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) axum::Server::bind(&addr)
.serve(app.into_make_service()) .serve(app.into_make_service())

View File

@@ -31,3 +31,9 @@ pub struct WebAuthInfo {
pub username: String, pub username: String,
pub remember_me: bool, pub remember_me: bool,
} }
#[derive(Serialize, Deserialize, Clone)]
pub struct ForgotPasswordRequest {
pub username: String,
pub contact_info: String,
}

View File

@@ -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<ServerState>,
Json(password_request): Json<ForgotPasswordRequest>,
) -> StatusCode {
StatusCode::OK
}

View File

@@ -1,6 +1,9 @@
use crate::models::auth::{Claims, LoginInfo, LoginResponse}; use crate::{
models::auth::{Claims, LoginInfo, LoginResponse},
server::server_state::ServerState,
};
use axum::{ use axum::{
extract::Json, extract::{Json, State},
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
}; };
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 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( pub async fn post_login_user(
State(state): State<ServerState>,
Json(login_info): Json<LoginInfo>, Json(login_info): Json<LoginInfo>,
) -> Result<Json<LoginResponse>, StatusCode> { ) -> Result<Json<LoginResponse>, StatusCode> {
let user_authenticated = is_valid_user(&login_info.username, &login_info.password); 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<Json<String>, StatusCode> { pub async fn post_test_login(
State(state): State<ServerState>,
header_map: HeaderMap,
) -> Result<Json<String>, StatusCode> {
if let Some(auth_header) = header_map.get("Authorization") { if let Some(auth_header) = header_map.get("Authorization") {
if let Ok(auth_header_str) = auth_header.to_str() { if let Ok(auth_header_str) = auth_header.to_str() {
if auth_header_str.starts_with("Bearer ") { if auth_header_str.starts_with("Bearer ") {

View File

@@ -1 +1,2 @@
pub mod forgot_password;
pub mod login; pub mod login;

View File

@@ -1,2 +1,3 @@
pub mod auth; pub mod auth;
pub mod routes; pub mod routes;
pub mod server_state;

View File

@@ -1,11 +1,21 @@
// (Server only) Routes // (Server only) Routes
use crate::endpoints::{LOGIN, LOGIN_TEST}; use crate::endpoints::{FORGOT_PASSWORD, LOGIN, LOGIN_TEST};
use axum::routing::{post, Router}; 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 { pub fn get_api_router(state: ServerState) -> Router {
let app = app.route(LOGIN, post(post_login_user)); Router::new()
let app = app.route(LOGIN_TEST, post(post_test_login)); .route(LOGIN, post(post_login_user))
app .route(LOGIN_TEST, post(post_test_login))
.route(FORGOT_PASSWORD, post(post_forgot_password))
.with_state(state)
} }

View File

@@ -0,0 +1,6 @@
use sea_orm::DatabaseConnection;
#[derive(Clone)]
pub struct ServerState {
pub db_conn: DatabaseConnection,
}