diff --git a/migrations/20230426221940_init.up.sql b/migrations/20230426221940_init.up.sql index 7d53573..02df49a 100644 --- a/migrations/20230426221940_init.up.sql +++ b/migrations/20230426221940_init.up.sql @@ -5,7 +5,7 @@ -- users create table if not exists witches ( - id blob not null primary key, + id int not null primary key, username text not null unique, displayname text, email text, diff --git a/src/db.rs b/src/db.rs index c9a55d9..e3ccc45 100644 --- a/src/db.rs +++ b/src/db.rs @@ -20,12 +20,12 @@ pub async fn get_pool() -> SqlitePool { let conn_opts = SqliteConnectOptions::new() .foreign_keys(true) .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Incremental) - .filename(&db_filename); + .filename(&db_filename) + .busy_timeout(Duration::from_secs(TIMEOUT)); // setup connection pool SqlitePoolOptions::new() .max_connections(MAX_CONNS) - .connect_timeout(Duration::from_secs(TIMEOUT)) .connect_with(conn_opts) .await .expect("can't connect to database") diff --git a/src/main.rs b/src/main.rs index 5e83589..66c0783 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use witch_watch::{ db, generic_handlers::{handle_slash, handle_slash_redir}, session_store::SqliteSessionStore, - users::{get_create_user, handle_signup_success, post_create_user}, + users::{get_create_user, get_login, handle_signup_success, post_create_user, post_login}, }; #[tokio::main] @@ -22,17 +22,18 @@ async fn main() { .init(); let pool = db::get_pool().await; - let store = SqliteSessionStore::from_client(pool.clone()); - store.migrate().await.expect("Could not migrate session DB"); - let secret = { - let mut bytes = [0u8; 128]; - let mut rng = OsRng; - rng.fill_bytes(&mut bytes); - bytes + let session_layer = { + let store = SqliteSessionStore::from_client(pool.clone()); + store.migrate().await.expect("Could not migrate session DB"); + let secret = { + let mut bytes = [0u8; 128]; + let mut rng = OsRng; + rng.fill_bytes(&mut bytes); + bytes + }; + SessionLayer::new(store, &secret).with_secure(true) }; - let session_layer = SessionLayer::new(store, &secret).with_secure(true); - // build our application with some routes let app = Router::new() .route("/", get(handle_slash).post(handle_slash)) .route("/signup", get(get_create_user).post(post_create_user)) @@ -40,6 +41,7 @@ async fn main() { "/signup_success/:id", get(handle_signup_success).post(handle_signup_success), ) + .route("/login", get(get_login).post(post_login)) .fallback(handle_slash_redir) .layer(session_layer) .with_state(pool); diff --git a/src/templates.rs b/src/templates.rs index afbc50d..f8ad15f 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,7 +1,7 @@ use askama::Template; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Template, Deserialize)] +#[derive(Debug, Default, Template, Deserialize, Serialize)] #[template(path = "signup.html")] pub struct CreateUser { pub username: String, @@ -10,3 +10,17 @@ pub struct CreateUser { pub password: String, pub pw_verify: String, } + +#[derive(Debug, Default, Template, Deserialize, Serialize)] +#[template(path = "login_post.html")] +pub struct LoginPost { + pub username: String, + pub password: String, +} + +#[derive(Debug, Default, Template, Deserialize, Serialize)] +#[template(path = "login_get.html")] +pub struct LoginGet { + pub username: String, + pub password: String, +} diff --git a/src/users.rs b/src/users.rs index 34a2a05..c729ccc 100644 --- a/src/users.rs +++ b/src/users.rs @@ -10,23 +10,25 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; -use axum_login::{ - secrecy::{SecretVec}, AuthUser, -}; -use sqlx::{query_as, sqlite::SqliteRow, Row, SqlitePool}; +use axum_login::{secrecy::SecretVec, AuthUser, SqliteStore}; +use rand_core::CryptoRngCore; +use sqlx::{query_as, SqlitePool}; use unicode_segmentation::UnicodeSegmentation; use uuid::Uuid; -use crate::{templates::CreateUser, ToBlob}; +use crate::{ + templates::{CreateUser, LoginGet}, + ToBlob, +}; const CREATE_QUERY: &str = "insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; const ID_QUERY: &str = "select * from witches where id = $1"; // const PW_QUERY: &str = "select pwhash from witches where id = $1"; -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq, sqlx::FromRow)] pub struct User { - pub id: Uuid, + pub id: i64, pub username: String, pub displayname: Option, pub email: Option, @@ -47,8 +49,10 @@ impl Display for User { } } -impl AuthUser for User { - fn get_id(&self) -> Uuid { +pub type AuthContext = axum_login::extractors::AuthContext>; + +impl AuthUser for User { + fn get_id(&self) -> i64 { self.id } @@ -57,33 +61,49 @@ impl AuthUser for User { } } +//-------------------------------------------------------------------------- +// Result types for user creation +//-------------------------------------------------------------------------- + #[derive(Debug, Clone, Template)] #[template(path = "signup_success.html")] pub struct CreateUserSuccess(User); -impl sqlx::FromRow<'_, SqliteRow> for User { - fn from_row(row: &SqliteRow) -> Result { - let bytes: Vec = row.get("id"); - let bytes = bytes.as_slice(); - let bytes: [u8; 16] = bytes.try_into().unwrap(); - let id = Uuid::from_bytes_le(bytes); - let username: String = row.get("username"); - let displayname: Option = row.get("displayname"); - let last_seen: Option = row.get("last_seen"); - let email: Option = row.get("email"); - let pwhash: String = row.get("pwhash"); +#[Error(desc = "Could not create user.")] +#[non_exhaustive] +pub struct CreateUserError(#[from] CreateUserErrorKind); - Ok(Self { - id, - username, - displayname, - email, - last_seen, - pwhash, - }) +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 { + AlreadyExists, + #[error(desc = "Usernames must be between 1 and 20 non-whitespace characters long")] + BadUsername, + PasswordMismatch, + #[error(desc = "Password must have at least 4 and at most 50 characters")] + BadPassword, + #[error(desc = "Display name must be less than 100 characters long")] + BadDisplayname, + BadEmail, + MissingFields, + UnknownDBError, +} + +//-------------------------------------------------------------------------- +// User creation route handlers +//-------------------------------------------------------------------------- + /// Get Handler: displays the form to create a user pub async fn get_create_user() -> CreateUser { CreateUser::default() @@ -109,12 +129,6 @@ pub async fn post_create_user( return Err(CreateUserErrorKind::BadUsername.into()); } - if let Some(ref dn) = displayname { - if dn.len() > 50 { - return Err(CreateUserErrorKind::BadDisplayname.into()); - } - } - if password != verify { return Err(CreateUserErrorKind::PasswordMismatch.into()); } @@ -122,12 +136,21 @@ pub async fn post_create_user( let password = urlencoding::decode(password) .map_err(|_| CreateUserErrorKind::BadPassword)? .to_string(); + let password = password.trim(); let password = password.as_bytes(); + if !(4..=50).contains(&password.len()) { + return Err(CreateUserErrorKind::BadPassword.into()); + } let displayname = if let Some(dn) = displayname { let dn = urlencoding::decode(dn) .map_err(|_| CreateUserErrorKind::BadDisplayname)? + .to_string() + .trim() .to_string(); + if dn.graphemes(true).size_hint().1.unwrap() > 100 { + return Err(CreateUserErrorKind::BadDisplayname.into()); + } Some(dn) } else { None @@ -147,7 +170,7 @@ pub async fn post_create_user( let user = create_user(username, displayname, email, password, &pool).await?; tracing::debug!("created {user:?}"); - let id = user.id.simple().to_string(); + let id = user.id; let location = format!("/signup_success/{id}"); let resp = axum::response::Redirect::temporary(&location).into_response(); @@ -180,6 +203,54 @@ pub async fn handle_signup_success( resp } +//-------------------------------------------------------------------------- +// Login error and success types +//-------------------------------------------------------------------------- + +#[Error] +pub struct LoginError(#[from] LoginErrorKind); + +#[Error] +#[non_exhaustive] +pub enum LoginErrorKind { + BadPassword, + Unknown, +} + +impl IntoResponse for LoginError { + fn into_response(self) -> Response { + match self.0 { + LoginErrorKind::Unknown => ( + StatusCode::INTERNAL_SERVER_ERROR, + "An unknown error occurred; you cursed, brah?", + ) + .into_response(), + _ => (StatusCode::BAD_REQUEST, format!("{self}")).into_response(), + } + } +} + +//-------------------------------------------------------------------------- +// Login handlers +//-------------------------------------------------------------------------- + +/// Handle login queries +#[axum::debug_handler] +pub async fn post_login( + mut auth: AuthContext, + State(pool): State, +) -> Result<(), LoginError> { + Err(LoginErrorKind::Unknown.into()) +} + +pub async fn get_login() -> impl IntoResponse { + LoginGet::default() +} + +//------------------------------------------------------------------------- +// private fns +//------------------------------------------------------------------------- + async fn create_user( username: &str, displayname: &Option, @@ -195,9 +266,10 @@ async fn create_user( .unwrap() // safe to unwrap, we know the salt is valid .to_string(); - let id = Uuid::new_v4(); + let mut rng = OsRng; + let id: i64 = rng.as_rngcore().next_u64() as i64; let res = sqlx::query(CREATE_QUERY) - .bind(id.blob()) + .bind(id) .bind(username) .bind(displayname) .bind(email) @@ -233,32 +305,3 @@ async fn create_user( _ => Err(CreateUserErrorKind::UnknownDBError.into()), } } - -#[Error(desc = "Could not 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 { - AlreadyExists, - #[error(desc = "Usernames must be between 1 and 20 non-whitespace characters long")] - BadUsername, - PasswordMismatch, - BadPassword, - BadDisplayname, - BadEmail, - MissingFields, - UnknownDBError, -} diff --git a/templates/login_get.html b/templates/login_get.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/login_post.html b/templates/login_post.html new file mode 100644 index 0000000..e69de29