Add basic register and login
All checks were successful
Build Crate / build (push) Successful in 1m45s
All checks were successful
Build Crate / build (push) Successful in 1m45s
This commit is contained in:
@@ -19,7 +19,6 @@ once_cell = "1.18.0"
|
|||||||
web-sys = { version = "0.3.64", features = ["Window", "Storage"] }
|
web-sys = { version = "0.3.64", features = ["Window", "Storage"] }
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
chrono = { version = "0.4.38", features = ["serde", "wasm-bindgen"] }
|
chrono = { version = "0.4.38", features = ["serde", "wasm-bindgen"] }
|
||||||
password-auth = "1.0.0"
|
|
||||||
lazy_static = "1.5"
|
lazy_static = "1.5"
|
||||||
|
|
||||||
[target.'cfg(engine)'.dev-dependencies]
|
[target.'cfg(engine)'.dev-dependencies]
|
||||||
@@ -38,6 +37,7 @@ sea-orm = { version = "1.0", features = [
|
|||||||
"with-chrono",
|
"with-chrono",
|
||||||
] }
|
] }
|
||||||
jsonwebtoken = "9.3.0"
|
jsonwebtoken = "9.3.0"
|
||||||
|
argon2 = "0.5"
|
||||||
|
|
||||||
[target.'cfg(client)'.dependencies]
|
[target.'cfg(client)'.dependencies]
|
||||||
wasm-bindgen = "0.2.93"
|
wasm-bindgen = "0.2.93"
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ impl MigrationTrait for Migration {
|
|||||||
Table::create()
|
Table::create()
|
||||||
.table(User::Table)
|
.table(User::Table)
|
||||||
.col(pk_auto(User::Id))
|
.col(pk_auto(User::Id))
|
||||||
.col(string(User::Username))
|
.col(string_uniq(User::Username))
|
||||||
.col(string(User::Password))
|
.col(string(User::PasswordHashAndSalt))
|
||||||
.col(string(User::Salt))
|
.col(string_null(User::Nickname))
|
||||||
.col(timestamp_with_time_zone(User::CreationTime))
|
.col(timestamp(User::CreationTime))
|
||||||
.col(timestamp_with_time_zone(User::LastActiveTime))
|
.col(timestamp(User::LastActiveTime))
|
||||||
.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))
|
||||||
@@ -41,8 +41,8 @@ pub enum User {
|
|||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
Username,
|
Username,
|
||||||
Password,
|
PasswordHashAndSalt,
|
||||||
Salt,
|
Nickname,
|
||||||
CreationTime,
|
CreationTime,
|
||||||
LastActiveTime,
|
LastActiveTime,
|
||||||
IsAdmin,
|
IsAdmin,
|
||||||
|
|||||||
@@ -145,14 +145,14 @@ fn login_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"){"Sign in"}
|
||||||
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"){"Password"}
|
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"){}
|
input (bind:value = state.password, type = "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 justify-between"){
|
||||||
(match props.remember_me {
|
(match props.remember_me {
|
||||||
|
|||||||
@@ -1 +1,181 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use perseus::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
use web_sys::Event;
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(client)] {
|
||||||
|
use crate::{
|
||||||
|
models::auth::{RegisterRequest},
|
||||||
|
endpoints::REGISTER,
|
||||||
|
state_enums::{LoginState, OpenState},
|
||||||
|
templates::{get_api_path},
|
||||||
|
global_state::{self, AppStateRx},
|
||||||
|
models::auth::WebAuthInfo,
|
||||||
|
};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref REGISTER_FORM: Capsule<PerseusNodeType, RegisterFormProps> = get_capsule();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||||
|
#[rx(alias = "RegisterFormStateRx")]
|
||||||
|
struct RegisterFormState {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
nickname: String,
|
||||||
|
registration_code: String,
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegisterFormStateRx {
|
||||||
|
#[cfg(client)]
|
||||||
|
fn reset(&self) {
|
||||||
|
self.username.set(String::new());
|
||||||
|
self.password.set(String::new());
|
||||||
|
self.nickname.set(String::new());
|
||||||
|
self.registration_code.set(String::new());
|
||||||
|
self.email.set(String::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RegisterFormProps {
|
||||||
|
pub nickname: bool,
|
||||||
|
pub registration_code: bool,
|
||||||
|
pub email: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_scope]
|
||||||
|
fn register_form_capsule<G: Html>(
|
||||||
|
cx: Scope,
|
||||||
|
state: &RegisterFormStateRx,
|
||||||
|
props: RegisterFormProps,
|
||||||
|
) -> View<G> {
|
||||||
|
let close_modal = move |_event: Event| {
|
||||||
|
#[cfg(client)]
|
||||||
|
{
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
state.reset();
|
||||||
|
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||||
|
global_state.modals_open.register.set(OpenState::Closed)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle_register = move |_event: Event| {
|
||||||
|
#[cfg(client)]
|
||||||
|
{
|
||||||
|
let registration_code = state.registration_code.get().as_ref().clone();
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
let register_info = RegisterRequest {
|
||||||
|
username: state.username.get().as_ref().clone(),
|
||||||
|
password: state.password.get().as_ref().clone(),
|
||||||
|
nickname: state.nickname.get().as_ref().clone(),
|
||||||
|
email: state.email.get().as_ref().clone(),
|
||||||
|
registration_code,
|
||||||
|
};
|
||||||
|
|
||||||
|
// // @todo clean up error handling
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.post(get_api_path(REGISTER).as_str())
|
||||||
|
.json(®ister_info)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||||
|
|
||||||
|
if response.status() != StatusCode::OK {
|
||||||
|
// todo update to some type of alert
|
||||||
|
state.username.set(response.status().to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open login modal
|
||||||
|
global_state.modals_open.login.set(OpenState::Open);
|
||||||
|
state.reset();
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
state.reset();
|
||||||
|
global_state.modals_open.register.set(OpenState::Closed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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") {
|
||||||
|
div (class="bg-white rounded-lg shadow relative dark:bg-gray-700"){
|
||||||
|
div (class="flex justify-end p-2"){
|
||||||
|
button (on:click = close_modal, class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white"){
|
||||||
|
"Back"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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"){"Register"}
|
||||||
|
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"){"Password"}
|
||||||
|
input (bind:value = state.password, type = "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"){}
|
||||||
|
}
|
||||||
|
(match props.registration_code {
|
||||||
|
true => { view!{cx,
|
||||||
|
div {
|
||||||
|
label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300"){"Registration code"}
|
||||||
|
input (bind:value = state.registration_code, 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"){}
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
false => {view!{cx,}},
|
||||||
|
})
|
||||||
|
(match props.nickname {
|
||||||
|
true => { view!{cx,
|
||||||
|
div {
|
||||||
|
label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300"){"Nickname (optional)"}
|
||||||
|
input (bind:value = state.nickname, 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"){}
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
false => {view!{cx,}},
|
||||||
|
})
|
||||||
|
(match props.email {
|
||||||
|
true => { view!{cx,
|
||||||
|
div {
|
||||||
|
label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300"){"Email (optional)"}
|
||||||
|
input (bind:value = state.email, 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"){}
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
false => {view!{cx,}},
|
||||||
|
})
|
||||||
|
button (on:click = handle_register, 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"){"Register"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_capsule<G: Html>() -> Capsule<G, RegisterFormProps> {
|
||||||
|
Capsule::build(Template::build("register_form").build_state_fn(get_build_state))
|
||||||
|
.empty_fallback()
|
||||||
|
.view_with_state(register_form_capsule)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[engine_only_fn]
|
||||||
|
async fn get_build_state(_info: StateGeneratorInfo<()>) -> RegisterFormState {
|
||||||
|
RegisterFormState {
|
||||||
|
username: String::new(),
|
||||||
|
password: String::new(),
|
||||||
|
nickname: String::new(),
|
||||||
|
registration_code: String::new(),
|
||||||
|
email: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ pub fn Header<'a, G: Html>(cx: Scope<'a>, HeaderProps { game, title }: HeaderPro
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let handle_log_out = move |_event: Event| {
|
let handle_log_out = move |_event: Event| {
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
{
|
{
|
||||||
@@ -61,7 +71,7 @@ pub fn Header<'a, G: Html>(cx: Scope<'a>, HeaderProps { game, title }: HeaderPro
|
|||||||
match *global_state.auth.state.get() {
|
match *global_state.auth.state.get() {
|
||||||
LoginState::NotAuthenticated => {
|
LoginState::NotAuthenticated => {
|
||||||
view! { cx,
|
view! { cx,
|
||||||
button(class = "text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700") {
|
button(on:click = handle_register, class = "text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700") {
|
||||||
"Register"
|
"Register"
|
||||||
}
|
}
|
||||||
button(on:click = handle_log_in, class = "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 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800") {
|
button(on:click = handle_log_in, class = "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 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800") {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::{
|
|||||||
capsules::{
|
capsules::{
|
||||||
forgot_password_form::{ForgotPasswordFormProps, FORGOT_PASSWORD_FORM},
|
forgot_password_form::{ForgotPasswordFormProps, FORGOT_PASSWORD_FORM},
|
||||||
login_form::{LoginFormProps, LOGIN_FORM},
|
login_form::{LoginFormProps, LOGIN_FORM},
|
||||||
|
register_form::{RegisterFormProps, REGISTER_FORM},
|
||||||
},
|
},
|
||||||
components::header::{Header, HeaderProps},
|
components::header::{Header, HeaderProps},
|
||||||
global_state::AppStateRx,
|
global_state::AppStateRx,
|
||||||
@@ -57,6 +58,22 @@ pub fn Layout<'a, G: Html>(
|
|||||||
view!{ cx, }
|
view!{ cx, }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
(match *global_state.modals_open.register.get() {
|
||||||
|
OpenState::Open => {
|
||||||
|
view! { cx,
|
||||||
|
(REGISTER_FORM.widget(cx, "",
|
||||||
|
RegisterFormProps{
|
||||||
|
registration_code: true,
|
||||||
|
nickname: true,
|
||||||
|
email: true,
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenState::Closed => {
|
||||||
|
view!{ cx, }
|
||||||
|
}
|
||||||
|
})
|
||||||
(match *global_state.modals_open.forgot_password.get() {
|
(match *global_state.modals_open.forgot_password.get() {
|
||||||
OpenState::Open => {
|
OpenState::Open => {
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pub const REGISTER: &str = "/api/login";
|
pub const REGISTER: &str = "/api/register";
|
||||||
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";
|
pub const FORGOT_PASSWORD: &str = "/api/forgot-password";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
|
||||||
|
|
||||||
use super::sea_orm_active_enums::GameType;
|
use super::sea_orm_active_enums::GameType;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
|
||||||
|
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
|
||||||
|
|
||||||
pub use super::game::Entity as Game;
|
pub use super::game::Entity as Game;
|
||||||
pub use super::game_to_team_result::Entity as GameToTeamResult;
|
pub use super::game_to_team_result::Entity as GameToTeamResult;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -8,11 +8,12 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
#[sea_orm(unique)]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password_hash_and_salt: String,
|
||||||
pub salt: String,
|
pub nickname: Option<String>,
|
||||||
pub creation_time: DateTimeWithTimeZone,
|
pub creation_time: DateTime,
|
||||||
pub last_active_time: DateTimeWithTimeZone,
|
pub last_active_time: DateTime,
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ pub fn main<G: Html>() -> PerseusApp<G> {
|
|||||||
.template(crate::templates::overall_board::get_template())
|
.template(crate::templates::overall_board::get_template())
|
||||||
.capsule_ref(&*crate::capsules::login_form::LOGIN_FORM)
|
.capsule_ref(&*crate::capsules::login_form::LOGIN_FORM)
|
||||||
.capsule_ref(&*crate::capsules::forgot_password_form::FORGOT_PASSWORD_FORM)
|
.capsule_ref(&*crate::capsules::forgot_password_form::FORGOT_PASSWORD_FORM)
|
||||||
|
.capsule_ref(&*crate::capsules::register_form::REGISTER_FORM)
|
||||||
.error_views(crate::error_views::get_error_views())
|
.error_views(crate::error_views::get_error_views())
|
||||||
.index_view(|cx| {
|
.index_view(|cx| {
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ pub struct WebAuthInfo {
|
|||||||
pub remember_me: bool,
|
pub remember_me: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub email: String,
|
||||||
|
pub nickname: String,
|
||||||
|
pub registration_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct ForgotPasswordRequest {
|
pub struct ForgotPasswordRequest {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|||||||
19
src/models/generic.rs
Normal file
19
src/models/generic.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct GenericResponse {
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenericResponse {
|
||||||
|
pub fn ok() -> Self {
|
||||||
|
GenericResponse {
|
||||||
|
status: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn err(msg: &str) -> Self {
|
||||||
|
GenericResponse {
|
||||||
|
status: msg.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod generic;
|
||||||
|
|||||||
@@ -1,24 +1,60 @@
|
|||||||
|
use crate::entity::prelude::*;
|
||||||
|
use crate::models::auth::{Claims, LoginInfo, LoginResponse};
|
||||||
use crate::{
|
use crate::{
|
||||||
models::auth::{Claims, LoginInfo, LoginResponse},
|
entity::user::{self, Entity},
|
||||||
|
models::auth::RegisterRequest,
|
||||||
server::server_state::ServerState,
|
server::server_state::ServerState,
|
||||||
};
|
};
|
||||||
|
use argon2::password_hash::rand_core::OsRng;
|
||||||
|
use argon2::password_hash::SaltString;
|
||||||
|
use argon2::Argon2;
|
||||||
|
use argon2::PasswordHash;
|
||||||
|
use argon2::PasswordHasher;
|
||||||
|
use argon2::PasswordVerifier;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Json, State},
|
extract::{Json, State},
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
};
|
};
|
||||||
|
use futures::sink::Fanout;
|
||||||
|
use sea_orm::ColumnTrait;
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
|
use sea_orm::InsertResult;
|
||||||
|
use sea_orm::QueryFilter;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
|
||||||
pub fn is_valid_user(username: &str, password: &str) -> bool {
|
pub async fn credentials_are_correct(username: &str, password: &str, state: &ServerState) -> bool {
|
||||||
return true;
|
// Get user
|
||||||
|
let existing_user: Option<user::Model> = User::find()
|
||||||
|
.filter(user::Column::Username.eq(username))
|
||||||
|
.one(&state.db_conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let hash_to_check: String = match existing_user {
|
||||||
|
Some(user) => user.password_hash_and_salt,
|
||||||
|
None => {
|
||||||
|
// @todo make dummy password hash
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Argon2::default()
|
||||||
|
.verify_password(
|
||||||
|
password.as_bytes(),
|
||||||
|
&PasswordHash::new(hash_to_check.as_str()).unwrap(),
|
||||||
|
)
|
||||||
|
.is_ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_login_user(
|
pub async fn post_login_user(
|
||||||
State(state): State<ServerState>,
|
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 =
|
||||||
|
credentials_are_correct(&login_info.username, &login_info.password, &state);
|
||||||
|
|
||||||
match user_authenticated {
|
match user_authenticated.await {
|
||||||
false => Err(StatusCode::UNAUTHORIZED),
|
false => Err(StatusCode::UNAUTHORIZED),
|
||||||
true => {
|
true => {
|
||||||
let expires = match login_info.remember_me {
|
let expires = match login_info.remember_me {
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod forgot_password;
|
pub mod forgot_password;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod register;
|
||||||
|
|||||||
89
src/server/auth/register.rs
Normal file
89
src/server/auth/register.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use crate::entity::prelude::*;
|
||||||
|
use crate::models::generic::GenericResponse;
|
||||||
|
use argon2::password_hash::rand_core::OsRng;
|
||||||
|
use argon2::password_hash::SaltString;
|
||||||
|
use argon2::Argon2;
|
||||||
|
use argon2::PasswordHash;
|
||||||
|
use argon2::PasswordHasher;
|
||||||
|
use axum::{extract::State, http::StatusCode, Json};
|
||||||
|
use chrono::Utc;
|
||||||
|
use sea_orm::ColumnTrait;
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
|
use sea_orm::InsertResult;
|
||||||
|
use sea_orm::QueryFilter;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entity::user::{self, Entity},
|
||||||
|
models::auth::RegisterRequest,
|
||||||
|
server::server_state::ServerState,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn post_register_user(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Json(register_info): Json<RegisterRequest>,
|
||||||
|
) -> (StatusCode, Json<GenericResponse>) {
|
||||||
|
// TODO -> update to use env, maybe prevent brute force too
|
||||||
|
if register_info.registration_code != "ferris" {
|
||||||
|
return (
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(GenericResponse::err("Incorrect registration code")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if username already exists
|
||||||
|
let username = register_info.username;
|
||||||
|
let existing_user: Option<user::Model> = User::find()
|
||||||
|
.filter(user::Column::Username.eq(username.clone()))
|
||||||
|
.one(&state.db_conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if existing_user.is_some() {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(GenericResponse::err("Username already exists")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate password
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let password_hash = argon2
|
||||||
|
.hash_password(register_info.password.as_bytes(), &salt)
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
let phc_string = PasswordHash::new(&password_hash).unwrap().to_string();
|
||||||
|
|
||||||
|
// If the username doen't exist, create the user
|
||||||
|
let new_user = user::ActiveModel {
|
||||||
|
username: Set(username),
|
||||||
|
password_hash_and_salt: Set(phc_string),
|
||||||
|
nickname: Set({
|
||||||
|
if register_info.nickname == "" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(register_info.nickname)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
creation_time: Set(Utc::now().naive_utc()),
|
||||||
|
last_active_time: Set(Utc::now().naive_utc()),
|
||||||
|
is_admin: Set(false),
|
||||||
|
email: Set({
|
||||||
|
if register_info.email == "" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(register_info.email)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
avatar: Set(None),
|
||||||
|
forgot_password_request: Set(None),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// TODO -> error handling
|
||||||
|
let db_resp = user::Entity::insert(new_user)
|
||||||
|
.exec(&state.db_conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
return (StatusCode::OK, Json(GenericResponse::ok()));
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// (Server only) Routes
|
// (Server only) Routes
|
||||||
use crate::endpoints::{FORGOT_PASSWORD, LOGIN, LOGIN_TEST};
|
use crate::endpoints::{FORGOT_PASSWORD, LOGIN, LOGIN_TEST, REGISTER};
|
||||||
use axum::routing::{post, Router};
|
use axum::routing::{post, Router};
|
||||||
use futures::executor::block_on;
|
use futures::executor::block_on;
|
||||||
use sea_orm::Database;
|
use sea_orm::Database;
|
||||||
@@ -8,12 +8,14 @@ use super::{
|
|||||||
auth::{
|
auth::{
|
||||||
forgot_password::post_forgot_password,
|
forgot_password::post_forgot_password,
|
||||||
login::{post_login_user, post_test_login},
|
login::{post_login_user, post_test_login},
|
||||||
|
register::post_register_user,
|
||||||
},
|
},
|
||||||
server_state::ServerState,
|
server_state::ServerState,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn get_api_router(state: ServerState) -> Router {
|
pub fn get_api_router(state: ServerState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route(REGISTER, post(post_register_user))
|
||||||
.route(LOGIN, post(post_login_user))
|
.route(LOGIN, post(post_login_user))
|
||||||
.route(LOGIN_TEST, post(post_test_login))
|
.route(LOGIN_TEST, post(post_test_login))
|
||||||
.route(FORGOT_PASSWORD, post(post_forgot_password))
|
.route(FORGOT_PASSWORD, post(post_forgot_password))
|
||||||
|
|||||||
Reference in New Issue
Block a user