Working user signup, looks ugly.
Still no login or sessions, though.
This commit is contained in:
parent
94f1b35c03
commit
091ddbf48a
8 changed files with 110 additions and 19 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -5,3 +5,5 @@ pub mod db;
|
|||
pub mod handlers;
|
||||
pub(crate) mod templates;
|
||||
pub mod users;
|
||||
|
||||
//pub type Db: axum::
|
||||
|
|
15
src/main.rs
15
src/main.rs
|
@ -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");
|
||||
|
|
|
@ -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,
|
||||
|
|
85
src/users.rs
85
src/users.rs
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div id="footer">
|
||||
{% block footer
|
||||
{% block footer %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue