Skip to content

Commit

Permalink
signup and login works :)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnbchron committed Feb 11, 2024
1 parent 9fc10a0 commit 83d3ba1
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 38 deletions.
28 changes: 27 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 45 additions & 7 deletions crates/auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use axum_login::{
AuthManagerLayer, AuthManagerLayerBuilder, AuthUser, AuthnBackend, UserId,
};
use color_eyre::eyre::Result;
use color_eyre::eyre::{eyre, Context, Result};
use serde::{Deserialize, Serialize};
use surrealdb::{engine::remote::ws::Client, sql::Thing};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthenticatedUser {
pub id: Thing,
pub name: String,
pub email: String,
pub pw_hash: String,
pub id: Thing,
pub name: String,
pub email: String,
pub password: String,
}

impl From<AuthenticatedUser> for auth_types::User {
Expand All @@ -27,7 +27,7 @@ impl AuthUser for AuthenticatedUser {
type Id = Thing;

fn id(&self) -> Self::Id { self.id.clone() }
fn session_auth_hash(&self) -> &[u8] { self.pw_hash.as_bytes() }
fn session_auth_hash(&self) -> &[u8] { self.password.as_bytes() }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand All @@ -47,6 +47,44 @@ impl Backend {
surreal_client: clients::surreal::SurrealRootClient::new().await?,
})
}

pub async fn signup(
&self,
name: String,
email: String,
password: String,
) -> Result<AuthenticatedUser> {
(*self.surreal_client).use_ns("main").await?;
(*self.surreal_client).use_db("main").await?;

// check whether a user with the given email already exists
let user: Option<AuthenticatedUser> = (*self.surreal_client)
.query("SELECT * FROM users WHERE email = $email")
.bind(("email", &email))
.await?
.take(0)
.wrap_err("Failed to query SurrealDB for existing user")?;

if user.is_some() {
return Err(eyre!("User with email {} already exists", email));
}

// create a new user
let user: Option<AuthenticatedUser> = (*self.surreal_client)
.query(
"CREATE user SET name = $name, email = $email, password = \
crypto::argon2::generate($password)",
)
.bind(("name", &name))
.bind(("email", &email))
.bind(("password", &password))
.await
.wrap_err("Failed to create user in SurrealDB")?
.take(0)
.wrap_err("Failed to insert user into SurrealDB")?;

user.ok_or_else(|| eyre!("Failed to create user"))
}
}

#[async_trait::async_trait]
Expand All @@ -64,7 +102,7 @@ impl AuthnBackend for Backend {

let user: Option<AuthenticatedUser> = (*self.surreal_client)
.query(
"SELECT * FROM users WHERE email = $email AND \
"SELECT * FROM user WHERE email = $email AND \
crypto::argon2::compare(password, $password)",
)
.bind(("email", &credentials.email))
Expand Down
2 changes: 2 additions & 0 deletions crates/site-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ cfg-if.workspace = true
thiserror.workspace = true
serde.workspace = true

validator = "0.16.1"

auth = { path = "../auth" }
auth_types = { path = "../auth_types" }

Expand Down
3 changes: 2 additions & 1 deletion crates/site-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pub fn App() -> impl IntoView {
<main>
<Routes>
<Route path="" view=pages::home_page::HomePage/>
<Route path="/login" view=pages::login_page::LoginPage/>
<Route path="/login" view=pages::auth::login_page::LoginPage/>
<Route path="/signup" view=pages::auth::signup_page::SignupPage/>
</Routes>
</main>
</Router>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use leptos::{logging::log, *};

use crate::pages::SmallPageWrapper;

#[island]
pub fn LoginPage() -> impl IntoView {
let (email, set_email) = create_signal(String::new());
Expand All @@ -9,7 +11,7 @@ pub fn LoginPage() -> impl IntoView {
let value = login_action.value();

view! {
<super::SmallPageWrapper>
<SmallPageWrapper>
<div class="d-card-body">
<p class="d-card-title text-2xl">"Login"</p>

Expand Down Expand Up @@ -46,7 +48,7 @@ pub fn LoginPage() -> impl IntoView {
}>"Login"</button>
</div>
</div>
</super::SmallPageWrapper>
</SmallPageWrapper>
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/site-app/src/pages/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod login_page;
pub mod signup_page;
184 changes: 184 additions & 0 deletions crates/site-app/src/pages/auth/signup_page.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use leptos::{logging::log, *};

use crate::pages::SmallPageWrapper;

#[island]
pub fn SignupPage() -> impl IntoView {
let (name, set_name) = create_signal(String::new());
let (email, set_email) = create_signal(String::new());
let (password, set_password) = create_signal(String::new());
let (confirm, set_confirm) = create_signal(String::new());

let name_validity = move || {
name.clone().with(|name| {
if name.is_empty() {
return None;
}
if name.len() < 3 {
return Some(
view! { <p class="d-label text-error">"Name must be at least 3 characters long"</p> }
.into_view(),
);
}
None
})
};

let email_validity = move || {
email.clone().with(|email| {
if email.is_empty() {
return None;
}
Some(match validator::validate_email(email) {
true => view! {}.into_view(),
false => {
view! { <p class="d-label text-error">"Invalid email address"</p> }
.into_view()
}
})
})
};

let password_validity = move || {
password.clone().with(|password| {
if password.is_empty() {
return None;
}
if password.len() < 8 {
return Some(
view! { <p class="d-label text-error">"Password must be at least 8 characters long"</p> }
.into_view(),
);
}
None
})
};

let confirm_validity = move || {
confirm.clone().with(|confirm| {
if confirm.is_empty() {
return None;
}
if *confirm != password() {
return Some(
view! { <p class="d-label text-error">"Passwords do not match"</p> }
.into_view(),
);
}
None
})
};

let signup_action = create_server_action::<Signup>();
let value = signup_action.value();

view! {
<SmallPageWrapper>
<div class="d-card-body">
<p class="d-card-title text-2xl">"Sign Up"</p>

// name
<div class="d-form-control">
<label class="d-label">"Name"</label>
<input
type="text" class="d-input d-input-bordered w-full max-w-xs"
on:input=move |ev| {set_name(event_target_value(&ev))} prop:value=name
/>
{ name_validity }
</div>

// email
<div class="d-form-control">
<label class="d-label">"Email"</label>
<input
type="text" class="d-input d-input-bordered w-full max-w-xs"
on:input=move |ev| {set_email(event_target_value(&ev))} prop:value=email
/>
{ email_validity }
</div>

// password
<div class="d-form-control">
<label class="d-label">"Password"</label>
<input
type="password" class="d-input d-input-bordered w-full max-w-xs"
on:input=move |ev| {set_password(event_target_value(&ev))} prop:value=password
/>
{ password_validity }
</div>

// confirm password
<div class="d-form-control">
<label class="d-label">"Confirm Password"</label>
<input
type="password" class="d-input d-input-bordered w-full max-w-xs"
on:input=move |ev| {set_confirm(event_target_value(&ev))} prop:value=confirm
/>
{ confirm_validity }
</div>

{ move || value().map(|v| match v {
Err(e) => view! { <p class="text-error">{ e.to_string() }</p> }.into_view(),
Ok(_) => view! { <p class="text-success">"Signed up!"</p> }.into_view(),
})}

// submit button
<div class="mt-6"></div>
<div class="d-form-control">
<button class="d-btn d-btn-primary" on:click=move |_| {
signup_action.dispatch(Signup {
name: name(),
email: email(),
password: password(),
confirm: confirm()
});
}>"Sign Up"</button>
</div>
</div>
</SmallPageWrapper>
}
}

#[server(Signup)]
pub async fn login(
name: String,
email: String,
password: String,
confirm: String,
) -> Result<(), ServerFnError> {
if name.len() < 3 {
return Err(ServerFnError::new(
"Name must be at least 3 characters long",
));
}
if email.is_empty() {
return Err(ServerFnError::new("Email cannot be empty"));
}
if !validator::validate_email(&email) {
return Err(ServerFnError::new("Invalid email address"));
}
if password.is_empty() {
return Err(ServerFnError::new("Password cannot be empty"));
}
if password != confirm {
return Err(ServerFnError::new("Passwords do not match"));
}

let auth_session = use_context::<auth::AuthSession>()
.ok_or_else(|| ServerFnError::new("Failed to get auth session"))?;

auth_session
.backend
.signup(name, email.clone(), password.clone())
.await
.map_err(|e| {
logging::error!("Failed to sign up: {:?}", e);
ServerFnError::new("Failed to sign up")
})?;

let _login_result = crate::pages::auth::login_page::login(email, password)
.await
.map_err(|e| ServerFnError::new(format!("Failed to log in: {e}")))?;

Ok(())
}
15 changes: 15 additions & 0 deletions crates/site-app/src/pages/history.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#V2
SELECT * FROM users
DELETE users;
SELECT * FROM users
DELETE unique_email
INFO FOR DB
exit
LET $pass = crypto::argon2::generate("password");
RETURN $pass
RETURN 123
$pass
RETURN crypto::argon2::generate("password")
SELECT * FROM users
INFO FOR DB
SELECT * FROM user
Loading

0 comments on commit 83d3ba1

Please sign in to comment.