From c742b752140ab0eee6e353c779bd897042ba6739 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Tue, 19 Jul 2022 19:49:39 -0400 Subject: Big login system Still needs cookies, but those are coming! (and should be set anyways!) --- src/config.rs | 1 + src/errors.rs | 6 +++- src/handlers/auth.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/handlers/mod.rs | 16 +++++++++++ src/main.rs | 3 ++ src/models.rs | 12 ++++++++ 6 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/handlers/auth.rs create mode 100644 src/handlers/mod.rs create mode 100644 src/models.rs (limited to 'src') diff --git a/src/config.rs b/src/config.rs index 9bacc6a..4ceba75 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,6 +14,7 @@ pub struct Config { #[derive(Deserialize)] pub struct ServerConfig { pub bind_addr: String, + pub admin_email: String } #[derive(Deserialize, Clone)] diff --git a/src/errors.rs b/src/errors.rs index 19e0a8a..23ad8fa 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -14,7 +14,11 @@ pub enum ServiceError { #[error("Not Found")] NotFound, #[error("Axum: {0}")] - Axum(#[from] axum::Error) + Axum(#[from] axum::Error), + #[error("SQL: {0}")] + Sql(#[from] sqlx::Error), + #[error("Bcrypt: {0}")] + Bcrypt(#[from] bcrypt::BcryptError), } pub type StringResult = Result; diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs new file mode 100644 index 0000000..c4672aa --- /dev/null +++ b/src/handlers/auth.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use axum::{response::{IntoResponse, Html, Redirect}, Form, Extension}; +use axum_extra::extract::{PrivateCookieJar, cookie::Cookie}; +use serde::Deserialize; +use sqlx::{query, query_as}; +use tracing::debug; +use crate::{errors::ServiceError, State, models::DbUser}; +use chrono::prelude::*; + +#[derive(Deserialize, Debug)] +pub struct RegisterForm { + pub email: String, + pub prefname: String, + pub password: String, + #[serde(rename="password-confirm")] + pub password_confirm: String +} + +#[derive(Deserialize)] +pub struct LoginForm { + pub email: String, + pub password: String, +} + +pub async fn login() -> impl IntoResponse { + let mut buf = Vec::new(); + crate::templates::login_html(&mut buf).unwrap(); + + Html(buf) +} + +pub async fn login_post(Form(login): Form, state: Extension>, jar: PrivateCookieJar) -> Result<(PrivateCookieJar, Redirect), ServiceError> { + let mut conn = state.conn.acquire().await?; + + let user: DbUser = query_as("SELECT * FROM users WHERE email=$1").bind(login.email) + .fetch_one(&mut conn) + .await?; + + if bcrypt::verify(login.password, &user.pw_hash)? { + debug!("Logged in ID {} (email {})", user.id, user.email); + query("UPDATE users SET last_login=$1 WHERE id=$2").bind(Utc::now()).bind(user.id) + .execute(&mut conn) + .await?; + + let updated_jar = jar.add(Cookie::new("user-id", user.id.clone())); + } else { + + } + Ok((updated_jar, Redirect::to("/"))) +} + +pub async fn register() -> impl IntoResponse { + let mut buf = Vec::new(); + crate::templates::register_html(&mut buf).unwrap(); + + Html(buf) +} + +pub async fn register_post(Form(reg): Form, state: Extension>) -> impl IntoResponse { + if reg.password_confirm == reg.password { + let hash = match bcrypt::hash(reg.password, bcrypt::DEFAULT_COST) { + Ok(h) => h, + Err(e) => { + return Err(ServiceError::NotAuthorized); + } + }; + + let mut conn = state.conn.acquire().await?; + let ulid = ulid::Ulid::new(); + + query("INSERT INTO users (id, email, pref_name, pw_hash, last_login) VALUES ($1, $2, $3, $4, $5)").bind(ulid.to_string()).bind(reg.email.clone()).bind(reg.prefname).bind(hash).bind(Utc::now()) + .execute(&mut conn) + .await?; + + } + + Ok(Redirect::to("/dash/auth/login")) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..4076e68 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,16 @@ +use axum::{Router, routing::get}; + +mod auth; + +pub async fn gen_routers() -> Router { + + Router::new() + .nest("/auth", auth_routes().await) +} + +async fn auth_routes() -> Router { + + Router::new() + .route("/login", get(auth::login).post(auth::login_post)) + .route("/register", get(auth::register).post(auth::register_post)) +} diff --git a/src/main.rs b/src/main.rs index c4a298f..ed943c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ mod config; mod errors; +mod handlers; +mod models; use std::{net::SocketAddr, str::FromStr, sync::Arc, time::Duration}; @@ -50,6 +52,7 @@ async fn main() { let app = Router::new() .route("/health", get(health_check)) .route("/", get(index)) + .nest("/dash", handlers::gen_routers().await) .route("/static/:name", get(statics)) .layer( ServiceBuilder::new() diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..b367f4f --- /dev/null +++ b/src/models.rs @@ -0,0 +1,12 @@ +use sqlx::FromRow; + + + +#[derive(Debug, FromRow)] +pub struct DbUser { + pub id: String, + pub email: String, + pub pref_name: String, + pub pw_hash: String, + pub last_login: chrono::NaiveDateTime +} -- cgit v1.2.3