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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,11 +2,19 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use askama::Template;
use axum::{
extract::{Form, Path, State},
http::StatusCode,
response::IntoResponse,
};
use sqlx::{error::DatabaseError, Sqlite, SqlitePool};
use tracing::log::log;
use unicode_segmentation::UnicodeSegmentation;
use uuid::Uuid;
use crate::templates::CreateUser;
const CREATE_QUERY: &str =
"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(
username: &str,
displayname: &Option<String>,
email: &Option<String>,
password: &[u8],
pool: &SqlitePool,
) -> Result<User, CreateUserError> {
pub async fn get_create_user() -> CreateUser {
CreateUser::default()
}
#[axum::debug_handler]
pub async fn post_create_user(
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 name_len = username.graphemes(true).size_hint().1.unwrap();
// we are not ascii exclusivists around here
if !(1..=20).contains(&name_len) {
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)
let argon2 = Argon2::default();
let salt = SaltString::generate(&mut OsRng);
@ -103,6 +160,17 @@ pub async fn create_user(
#[non_exhaustive]
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]
#[non_exhaustive]
pub enum CreateUserErrorKind {
@ -110,6 +178,9 @@ pub enum CreateUserErrorKind {
#[error(desc = "Usernames must be between 1 and 20 non-whitespace characters long")]
BadUsername,
PasswordMismatch,
BadPassword,
BadDisplayname,
BadEmail,
MissingFields,
UnknownDBError,
}

View file

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

View file

@ -4,4 +4,18 @@
{% 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 %}