checkpoint working skeleton for login

This commit is contained in:
Joe Ardent 2023-05-28 12:20:55 -07:00
parent 151719daf1
commit b1fd57c486
7 changed files with 140 additions and 81 deletions

View file

@ -5,7 +5,7 @@
-- users -- users
create table if not exists witches ( create table if not exists witches (
id blob not null primary key, id int not null primary key,
username text not null unique, username text not null unique,
displayname text, displayname text,
email text, email text,

View file

@ -20,12 +20,12 @@ pub async fn get_pool() -> SqlitePool {
let conn_opts = SqliteConnectOptions::new() let conn_opts = SqliteConnectOptions::new()
.foreign_keys(true) .foreign_keys(true)
.auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Incremental) .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Incremental)
.filename(&db_filename); .filename(&db_filename)
.busy_timeout(Duration::from_secs(TIMEOUT));
// setup connection pool // setup connection pool
SqlitePoolOptions::new() SqlitePoolOptions::new()
.max_connections(MAX_CONNS) .max_connections(MAX_CONNS)
.connect_timeout(Duration::from_secs(TIMEOUT))
.connect_with(conn_opts) .connect_with(conn_opts)
.await .await
.expect("can't connect to database") .expect("can't connect to database")

View file

@ -8,7 +8,7 @@ use witch_watch::{
db, db,
generic_handlers::{handle_slash, handle_slash_redir}, generic_handlers::{handle_slash, handle_slash_redir},
session_store::SqliteSessionStore, 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] #[tokio::main]
@ -22,6 +22,7 @@ async fn main() {
.init(); .init();
let pool = db::get_pool().await; let pool = db::get_pool().await;
let session_layer = {
let store = SqliteSessionStore::from_client(pool.clone()); let store = SqliteSessionStore::from_client(pool.clone());
store.migrate().await.expect("Could not migrate session DB"); store.migrate().await.expect("Could not migrate session DB");
let secret = { let secret = {
@ -30,9 +31,9 @@ async fn main() {
rng.fill_bytes(&mut bytes); rng.fill_bytes(&mut bytes);
bytes bytes
}; };
let session_layer = SessionLayer::new(store, &secret).with_secure(true); SessionLayer::new(store, &secret).with_secure(true)
};
// build our application with some routes
let app = Router::new() let app = Router::new()
.route("/", get(handle_slash).post(handle_slash)) .route("/", get(handle_slash).post(handle_slash))
.route("/signup", get(get_create_user).post(post_create_user)) .route("/signup", get(get_create_user).post(post_create_user))
@ -40,6 +41,7 @@ async fn main() {
"/signup_success/:id", "/signup_success/:id",
get(handle_signup_success).post(handle_signup_success), get(handle_signup_success).post(handle_signup_success),
) )
.route("/login", get(get_login).post(post_login))
.fallback(handle_slash_redir) .fallback(handle_slash_redir)
.layer(session_layer) .layer(session_layer)
.with_state(pool); .with_state(pool);

View file

@ -1,7 +1,7 @@
use askama::Template; 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")] #[template(path = "signup.html")]
pub struct CreateUser { pub struct CreateUser {
pub username: String, pub username: String,
@ -10,3 +10,17 @@ pub struct CreateUser {
pub password: String, pub password: String,
pub pw_verify: 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,
}

View file

@ -10,23 +10,25 @@ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use axum_login::{ use axum_login::{secrecy::SecretVec, AuthUser, SqliteStore};
secrecy::{SecretVec}, AuthUser, use rand_core::CryptoRngCore;
}; use sqlx::{query_as, SqlitePool};
use sqlx::{query_as, sqlite::SqliteRow, Row, SqlitePool};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use uuid::Uuid; use uuid::Uuid;
use crate::{templates::CreateUser, ToBlob}; use crate::{
templates::{CreateUser, LoginGet},
ToBlob,
};
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)";
const ID_QUERY: &str = "select * from witches where id = $1"; const ID_QUERY: &str = "select * from witches where id = $1";
// const PW_QUERY: &str = "select pwhash 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 struct User {
pub id: Uuid, pub id: i64,
pub username: String, pub username: String,
pub displayname: Option<String>, pub displayname: Option<String>,
pub email: Option<String>, pub email: Option<String>,
@ -47,8 +49,10 @@ impl Display for User {
} }
} }
impl AuthUser<Uuid> for User { pub type AuthContext = axum_login::extractors::AuthContext<i64, User, SqliteStore<User>>;
fn get_id(&self) -> Uuid {
impl AuthUser<i64> for User {
fn get_id(&self) -> i64 {
self.id self.id
} }
@ -57,33 +61,49 @@ impl AuthUser<Uuid> for User {
} }
} }
//--------------------------------------------------------------------------
// Result types for user creation
//--------------------------------------------------------------------------
#[derive(Debug, Clone, Template)] #[derive(Debug, Clone, Template)]
#[template(path = "signup_success.html")] #[template(path = "signup_success.html")]
pub struct CreateUserSuccess(User); pub struct CreateUserSuccess(User);
impl sqlx::FromRow<'_, SqliteRow> for User { #[Error(desc = "Could not create user.")]
fn from_row(row: &SqliteRow) -> Result<Self, sqlx::Error> { #[non_exhaustive]
let bytes: Vec<u8> = row.get("id"); pub struct CreateUserError(#[from] CreateUserErrorKind);
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<String> = row.get("displayname");
let last_seen: Option<i64> = row.get("last_seen");
let email: Option<String> = row.get("email");
let pwhash: String = row.get("pwhash");
Ok(Self { impl IntoResponse for CreateUserError {
id, fn into_response(self) -> askama_axum::Response {
username, match self.0 {
displayname, CreateUserErrorKind::UnknownDBError => {
email, (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
last_seen, }
pwhash, _ => (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 /// Get Handler: displays the form to create a user
pub async fn get_create_user() -> CreateUser { pub async fn get_create_user() -> CreateUser {
CreateUser::default() CreateUser::default()
@ -109,12 +129,6 @@ pub async fn post_create_user(
return Err(CreateUserErrorKind::BadUsername.into()); return Err(CreateUserErrorKind::BadUsername.into());
} }
if let Some(ref dn) = displayname {
if dn.len() > 50 {
return Err(CreateUserErrorKind::BadDisplayname.into());
}
}
if password != verify { if password != verify {
return Err(CreateUserErrorKind::PasswordMismatch.into()); return Err(CreateUserErrorKind::PasswordMismatch.into());
} }
@ -122,12 +136,21 @@ pub async fn post_create_user(
let password = urlencoding::decode(password) let password = urlencoding::decode(password)
.map_err(|_| CreateUserErrorKind::BadPassword)? .map_err(|_| CreateUserErrorKind::BadPassword)?
.to_string(); .to_string();
let password = password.trim();
let password = password.as_bytes(); let password = password.as_bytes();
if !(4..=50).contains(&password.len()) {
return Err(CreateUserErrorKind::BadPassword.into());
}
let displayname = if let Some(dn) = displayname { let displayname = if let Some(dn) = displayname {
let dn = urlencoding::decode(dn) let dn = urlencoding::decode(dn)
.map_err(|_| CreateUserErrorKind::BadDisplayname)? .map_err(|_| CreateUserErrorKind::BadDisplayname)?
.to_string()
.trim()
.to_string(); .to_string();
if dn.graphemes(true).size_hint().1.unwrap() > 100 {
return Err(CreateUserErrorKind::BadDisplayname.into());
}
Some(dn) Some(dn)
} else { } else {
None None
@ -147,7 +170,7 @@ pub async fn post_create_user(
let user = create_user(username, displayname, email, password, &pool).await?; let user = create_user(username, displayname, email, password, &pool).await?;
tracing::debug!("created {user:?}"); tracing::debug!("created {user:?}");
let id = user.id.simple().to_string(); let id = user.id;
let location = format!("/signup_success/{id}"); let location = format!("/signup_success/{id}");
let resp = axum::response::Redirect::temporary(&location).into_response(); let resp = axum::response::Redirect::temporary(&location).into_response();
@ -180,6 +203,54 @@ pub async fn handle_signup_success(
resp 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<SqlitePool>,
) -> Result<(), LoginError> {
Err(LoginErrorKind::Unknown.into())
}
pub async fn get_login() -> impl IntoResponse {
LoginGet::default()
}
//-------------------------------------------------------------------------
// private fns
//-------------------------------------------------------------------------
async fn create_user( async fn create_user(
username: &str, username: &str,
displayname: &Option<String>, displayname: &Option<String>,
@ -195,9 +266,10 @@ async fn create_user(
.unwrap() // safe to unwrap, we know the salt is valid .unwrap() // safe to unwrap, we know the salt is valid
.to_string(); .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) let res = sqlx::query(CREATE_QUERY)
.bind(id.blob()) .bind(id)
.bind(username) .bind(username)
.bind(displayname) .bind(displayname)
.bind(email) .bind(email)
@ -233,32 +305,3 @@ async fn create_user(
_ => Err(CreateUserErrorKind::UnknownDBError.into()), _ => 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,
}

0
templates/login_get.html Normal file
View file

View file