Working user signup, looks ugly.

Still no login or sessions, though.
This commit is contained in:
Joe Ardent 2023-05-18 10:05:29 -07:00
parent 94f1b35c03
commit 091ddbf48a
8 changed files with 110 additions and 19 deletions

7
Cargo.lock generated
View file

@ -2447,6 +2447,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "0.8.2" version = "0.8.2"
@ -2793,6 +2799,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"unicode-segmentation", "unicode-segmentation",
"urlencoding",
"uuid 1.3.1", "uuid 1.3.1",
] ]

View file

@ -23,3 +23,4 @@ justerror = "1.1.0"
password-hash = { version = "0.5.0", features = ["std", "getrandom"] } password-hash = { version = "0.5.0", features = ["std", "getrandom"] }
axum-login = { version = "0.5.0", features = ["sqlite", "sqlx"] } axum-login = { version = "0.5.0", features = ["sqlite", "sqlx"] }
unicode-segmentation = "1.10.1" unicode-segmentation = "1.10.1"
urlencoding = "2.1.2"

View file

@ -5,3 +5,5 @@ pub mod db;
pub mod handlers; pub mod handlers;
pub(crate) mod templates; pub(crate) mod templates;
pub mod users; pub mod users;
//pub type Db: axum::

View file

@ -2,7 +2,10 @@ use std::net::SocketAddr;
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use witch_watch::{db, handlers}; use witch_watch::{
db,
users::{get_create_user, post_create_user},
};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -16,17 +19,9 @@ async fn main() {
let pool = db::get_pool().await; let pool = db::get_pool().await;
let _ = witch_watch::users::create_user("joe", &None, &None, &[], &pool)
.await
.unwrap();
// build our application with some routes // build our application with some routes
use handlers::*;
let app = Router::new() let app = Router::new()
.route( .route("/signup", get(get_create_user).post(post_create_user))
"/",
get(using_connection_pool_extractor).post(using_connection_extractor),
)
.with_state(pool); .with_state(pool);
tracing::debug!("binding to 0.0.0.0:3000"); tracing::debug!("binding to 0.0.0.0:3000");

View file

@ -1,6 +1,7 @@
use askama::Template; use askama::Template;
use serde::Deserialize;
#[derive(Template)] #[derive(Debug, Default, Template, Deserialize)]
#[template(path = "signup.html")] #[template(path = "signup.html")]
pub struct CreateUser { pub struct CreateUser {
pub username: String, pub username: String,

View file

@ -2,11 +2,19 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2, Argon2,
}; };
use askama::Template;
use axum::{
extract::{Form, Path, State},
http::StatusCode,
response::IntoResponse,
};
use sqlx::{error::DatabaseError, Sqlite, SqlitePool}; use sqlx::{error::DatabaseError, Sqlite, SqlitePool};
use tracing::log::log; use tracing::log::log;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use uuid::Uuid; use uuid::Uuid;
use crate::templates::CreateUser;
const CREATE_QUERY: &str = const CREATE_QUERY: &str =
"insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; "insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)";
@ -38,20 +46,69 @@ impl From<DbUser> for User {
} }
} }
pub async fn create_user( pub async fn get_create_user() -> CreateUser {
username: &str, CreateUser::default()
displayname: &Option<String>, }
email: &Option<String>,
password: &[u8], #[axum::debug_handler]
pool: &SqlitePool, pub async fn post_create_user(
) -> Result<User, CreateUserError> { State(pool): State<sqlx::Pool<Sqlite>>,
Form(signup): Form<CreateUser>,
) -> Result<(), CreateUserError> {
let username = &signup.username;
let displayname = &signup.displayname;
let email = &signup.email;
let password = &signup.password;
let verify = &signup.pw_verify;
let username = username.trim(); let username = username.trim();
let name_len = username.graphemes(true).size_hint().1.unwrap(); let name_len = username.graphemes(true).size_hint().1.unwrap();
// we are not ascii exclusivists around here // we are not ascii exclusivists around here
if !(1..=20).contains(&name_len) { if !(1..=20).contains(&name_len) {
return Err(CreateUserErrorKind::BadUsername.into()); return Err(CreateUserErrorKind::BadUsername.into());
} }
if password != verify {
return Err(CreateUserErrorKind::PasswordMismatch.into());
}
let password = urlencoding::decode(password)
.map_err(|_| CreateUserErrorKind::BadPassword)?
.to_string();
let password = password.as_bytes();
let displayname = if let Some(dn) = displayname {
let dn = urlencoding::decode(dn)
.map_err(|_| CreateUserErrorKind::BadDisplayname)?
.to_string();
Some(dn)
} else {
None
};
let displayname = &displayname;
// TODO(2023-05-17): validate email
let email = if let Some(email) = email {
let email = urlencoding::decode(email)
.map_err(|_| CreateUserErrorKind::BadEmail)?
.to_string();
Some(email)
} else {
None
};
let email = &email;
let _ = create_user(username, displayname, email, password, &pool).await?;
Ok(())
}
async fn create_user(
username: &str,
displayname: &Option<String>,
email: &Option<String>,
password: &[u8],
pool: &SqlitePool,
) -> Result<User, CreateUserError> {
// Argon2 with default params (Argon2id v19) // Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default(); let argon2 = Argon2::default();
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
@ -103,6 +160,17 @@ pub async fn create_user(
#[non_exhaustive] #[non_exhaustive]
pub struct CreateUserError(#[from] CreateUserErrorKind); pub struct CreateUserError(#[from] CreateUserErrorKind);
impl IntoResponse for CreateUserError {
fn into_response(self) -> askama_axum::Response {
match self.0 {
CreateUserErrorKind::UnknownDBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
_ => (StatusCode::BAD_REQUEST, format!("{self}")).into_response(),
}
}
}
#[Error] #[Error]
#[non_exhaustive] #[non_exhaustive]
pub enum CreateUserErrorKind { pub enum CreateUserErrorKind {
@ -110,6 +178,9 @@ pub enum CreateUserErrorKind {
#[error(desc = "Usernames must be between 1 and 20 non-whitespace characters long")] #[error(desc = "Usernames must be between 1 and 20 non-whitespace characters long")]
BadUsername, BadUsername,
PasswordMismatch, PasswordMismatch,
BadPassword,
BadDisplayname,
BadEmail,
MissingFields, MissingFields,
UnknownDBError, UnknownDBError,
} }

View file

@ -12,7 +12,7 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<div id="footer"> <div id="footer">
{% block footer {% block footer %}{% endblock %}
</div> </div>
</body> </body>
</html> </html>

View file

@ -4,4 +4,18 @@
{% block content %} {% block content %}
<form action="/signup" enctype="application/x-www-form-urlencoded" method="post">
<label for="username">Username</label>
<input type="text" name="username" id="username" minlength="1" maxlength="20" required>
<label for="displayname">Displayname (optional)</label>
<input type="text" name="displayname" id="displayname">
<label for="email">Email (optional)</label>
<input type="text" name="email">
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<label for="confirm_password">Confirm Password</label>
<input type="password" name="pw_verify" id="pw_verify" required>
<input type="submit" value="Signup">
</form>
{% endblock %} {% endblock %}