diff --git a/Cargo.toml b/Cargo.toml index 3ce9d44..365ac8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ once_cell = "1.18.0" web-sys = { version = "0.3.64", features = ["Window", "Storage"] } cfg-if = "1.0.0" chrono = { version = "0.4.38", features = ["serde", "wasm-bindgen"] } -password-auth = "1.0.0" lazy_static = "1.5" [target.'cfg(engine)'.dev-dependencies] @@ -38,6 +37,7 @@ sea-orm = { version = "1.0", features = [ "with-chrono", ] } jsonwebtoken = "9.3.0" +argon2 = "0.5" [target.'cfg(client)'.dependencies] wasm-bindgen = "0.2.93" diff --git a/migration/src/m20240813_000001_create_users.rs b/migration/src/m20240813_000001_create_users.rs index 1c6dcd2..d0edd7d 100644 --- a/migration/src/m20240813_000001_create_users.rs +++ b/migration/src/m20240813_000001_create_users.rs @@ -15,11 +15,11 @@ impl MigrationTrait for Migration { Table::create() .table(User::Table) .col(pk_auto(User::Id)) - .col(string(User::Username)) - .col(string(User::Password)) - .col(string(User::Salt)) - .col(timestamp_with_time_zone(User::CreationTime)) - .col(timestamp_with_time_zone(User::LastActiveTime)) + .col(string_uniq(User::Username)) + .col(string(User::PasswordHashAndSalt)) + .col(string_null(User::Nickname)) + .col(timestamp(User::CreationTime)) + .col(timestamp(User::LastActiveTime)) .col(boolean(User::IsAdmin)) .col(string_null(User::Email)) .col(string_null(User::Avatar)) @@ -41,8 +41,8 @@ pub enum User { Table, Id, Username, - Password, - Salt, + PasswordHashAndSalt, + Nickname, CreationTime, LastActiveTime, IsAdmin, diff --git a/src/capsules/login_form.rs b/src/capsules/login_form.rs index b0ab949..dd962a7 100644 --- a/src/capsules/login_form.rs +++ b/src/capsules/login_form.rs @@ -145,14 +145,14 @@ fn login_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"){"Sign in"} 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, 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"){ (match props.remember_me { diff --git a/src/capsules/register_form.rs b/src/capsules/register_form.rs index 8b13789..9190576 100644 --- a/src/capsules/register_form.rs +++ b/src/capsules/register_form.rs @@ -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 = 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( + cx: Scope, + state: &RegisterFormStateRx, + props: RegisterFormProps, +) -> View { + let close_modal = move |_event: Event| { + #[cfg(client)] + { + spawn_local_scoped(cx, async move { + state.reset(); + let global_state = Reactor::::from_cx(cx).get_global_state::(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::::from_cx(cx).get_global_state::(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() -> Capsule { + 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(), + } +} diff --git a/src/components/header.rs b/src/components/header.rs index 4d12dc4..9f0560c 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -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::::from_cx(cx).get_global_state::(cx); + global_state.modals_open.register.set(OpenState::Open); + }); + } + }; + let handle_log_out = move |_event: Event| { #[cfg(client)] { @@ -61,10 +71,10 @@ pub fn Header<'a, G: Html>(cx: Scope<'a>, HeaderProps { game, title }: HeaderPro match *global_state.auth.state.get() { LoginState::NotAuthenticated => { 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" } - 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") { "Log in" } } diff --git a/src/components/layout.rs b/src/components/layout.rs index 4870289..7734ca2 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -2,6 +2,7 @@ use crate::{ capsules::{ forgot_password_form::{ForgotPasswordFormProps, FORGOT_PASSWORD_FORM}, login_form::{LoginFormProps, LOGIN_FORM}, + register_form::{RegisterFormProps, REGISTER_FORM}, }, components::header::{Header, HeaderProps}, global_state::AppStateRx, @@ -57,6 +58,22 @@ pub fn Layout<'a, G: Html>( 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() { OpenState::Open => { view! { cx, diff --git a/src/endpoints.rs b/src/endpoints.rs index 87f1288..0b90741 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -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_TEST: &str = "/api/login-test"; pub const FORGOT_PASSWORD: &str = "/api/forgot-password"; diff --git a/src/entity/game.rs b/src/entity/game.rs index bd93b2a..18c89a5 100644 --- a/src/entity/game.rs +++ b/src/entity/game.rs @@ -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 sea_orm::entity::prelude::*; diff --git a/src/entity/game_to_team_result.rs b/src/entity/game_to_team_result.rs index 2aab72e..52040a4 100644 --- a/src/entity/game_to_team_result.rs +++ b/src/entity/game_to_team_result.rs @@ -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 serde::{Deserialize, Serialize}; diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 51b1436..8a33116 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -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; diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index d4c75d9..6f116a3 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -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_to_team_result::Entity as GameToTeamResult; diff --git a/src/entity/sea_orm_active_enums.rs b/src/entity/sea_orm_active_enums.rs index f3a45b7..25dba9d 100644 --- a/src/entity/sea_orm_active_enums.rs +++ b/src/entity/sea_orm_active_enums.rs @@ -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 serde::{Deserialize, Serialize}; diff --git a/src/entity/team_result.rs b/src/entity/team_result.rs index 602061a..d3bfadc 100644 --- a/src/entity/team_result.rs +++ b/src/entity/team_result.rs @@ -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 serde::{Deserialize, Serialize}; diff --git a/src/entity/team_result_to_user.rs b/src/entity/team_result_to_user.rs index 1d73820..7e73bed 100644 --- a/src/entity/team_result_to_user.rs +++ b/src/entity/team_result_to_user.rs @@ -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 serde::{Deserialize, Serialize}; diff --git a/src/entity/user.rs b/src/entity/user.rs index 6bb30a3..de767cd 100644 --- a/src/entity/user.rs +++ b/src/entity/user.rs @@ -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 serde::{Deserialize, Serialize}; @@ -8,11 +8,12 @@ use serde::{Deserialize, Serialize}; pub struct Model { #[sea_orm(primary_key)] pub id: i32, + #[sea_orm(unique)] pub username: String, - pub password: String, - pub salt: String, - pub creation_time: DateTimeWithTimeZone, - pub last_active_time: DateTimeWithTimeZone, + pub password_hash_and_salt: String, + pub nickname: Option, + pub creation_time: DateTime, + pub last_active_time: DateTime, pub is_admin: bool, pub email: Option, pub avatar: Option, diff --git a/src/main.rs b/src/main.rs index 6c81df4..4477403 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,6 +76,7 @@ pub fn main() -> PerseusApp { .template(crate::templates::overall_board::get_template()) .capsule_ref(&*crate::capsules::login_form::LOGIN_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()) .index_view(|cx| { view! { cx, diff --git a/src/models/auth.rs b/src/models/auth.rs index cc7349f..e1850f6 100644 --- a/src/models/auth.rs +++ b/src/models/auth.rs @@ -32,6 +32,15 @@ pub struct WebAuthInfo { 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)] pub struct ForgotPasswordRequest { pub username: String, diff --git a/src/models/generic.rs b/src/models/generic.rs new file mode 100644 index 0000000..61938b8 --- /dev/null +++ b/src/models/generic.rs @@ -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(), + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 0e4a05d..bcb51d1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1 +1,2 @@ pub mod auth; +pub mod generic; diff --git a/src/server/auth/login.rs b/src/server/auth/login.rs index c687a47..fec2d93 100644 --- a/src/server/auth/login.rs +++ b/src/server/auth/login.rs @@ -1,24 +1,60 @@ +use crate::entity::prelude::*; +use crate::models::auth::{Claims, LoginInfo, LoginResponse}; use crate::{ - models::auth::{Claims, LoginInfo, LoginResponse}, + entity::user::{self, Entity}, + models::auth::RegisterRequest, 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::{ extract::{Json, State}, 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}; -pub fn is_valid_user(username: &str, password: &str) -> bool { - return true; +pub async fn credentials_are_correct(username: &str, password: &str, state: &ServerState) -> bool { + // Get user + let existing_user: Option = 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( State(state): State, Json(login_info): Json, ) -> Result, 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), true => { let expires = match login_info.remember_me { diff --git a/src/server/auth/mod.rs b/src/server/auth/mod.rs index 85b2611..aa36f87 100644 --- a/src/server/auth/mod.rs +++ b/src/server/auth/mod.rs @@ -1,2 +1,3 @@ pub mod forgot_password; pub mod login; +pub mod register; diff --git a/src/server/auth/register.rs b/src/server/auth/register.rs new file mode 100644 index 0000000..e273a8a --- /dev/null +++ b/src/server/auth/register.rs @@ -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, + Json(register_info): Json, +) -> (StatusCode, Json) { + // 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::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())); +} diff --git a/src/server/routes.rs b/src/server/routes.rs index baf4a77..6efbe3b 100644 --- a/src/server/routes.rs +++ b/src/server/routes.rs @@ -1,5 +1,5 @@ // (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 futures::executor::block_on; use sea_orm::Database; @@ -8,12 +8,14 @@ use super::{ auth::{ forgot_password::post_forgot_password, login::{post_login_user, post_test_login}, + register::post_register_user, }, server_state::ServerState, }; pub fn get_api_router(state: ServerState) -> Router { Router::new() + .route(REGISTER, post(post_register_user)) .route(LOGIN, post(post_login_user)) .route(LOGIN_TEST, post(post_test_login)) .route(FORGOT_PASSWORD, post(post_forgot_password))