From 370a25e5a0cbb95e2aa1cec55305b22aeaf99aa0 Mon Sep 17 00:00:00 2001 From: cel 🌸 Date: Tue, 12 Dec 2023 14:14:30 +0000 Subject: initial refactor --- src/error.rs | 48 ++++++ src/lib.rs | 26 ++++ src/main.rs | 371 +++-------------------------------------------- src/notification.rs | 20 +++ src/routes/error.rs | 40 +++++ src/routes/home.rs | 21 +++ src/routes/login.rs | 82 +++++++++++ src/routes/logout.rs | 11 ++ src/routes/mod.rs | 7 + src/routes/signup.rs | 79 ++++++++++ src/routes/static.rs | 22 +++ src/routes/users.rs | 16 ++ src/users.rs | 13 ++ templates/base.rs.html | 2 +- templates/home.rs.html | 1 - templates/login.rs.html | 2 +- templates/signup.rs.html | 2 +- templates/users.rs.html | 2 +- 18 files changed, 406 insertions(+), 359 deletions(-) create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/notification.rs create mode 100644 src/routes/error.rs create mode 100644 src/routes/home.rs create mode 100644 src/routes/login.rs create mode 100644 src/routes/logout.rs create mode 100644 src/routes/mod.rs create mode 100644 src/routes/signup.rs create mode 100644 src/routes/static.rs create mode 100644 src/routes/users.rs create mode 100644 src/users.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e4999c8 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,48 @@ +use actix_session::{SessionGetError, SessionInsertError}; +use actix_web::{body::BoxBody, HttpResponse, ResponseError}; + +#[derive(Debug)] +pub enum PinussyError { + Database(sqlx::Error), + SessionInsertError, + SessionGetError, + Bcrypt, +} + +impl From for PinussyError { + fn from(_: SessionInsertError) -> Self { + Self::SessionInsertError + } +} + +impl From for PinussyError { + fn from(_: SessionGetError) -> Self { + Self::SessionGetError + } +} + +impl From for PinussyError { + fn from(e: sqlx::Error) -> Self { + Self::Database(e) + } +} + +impl From for PinussyError { + fn from(_: bcrypt::BcryptError) -> Self { + Self::Bcrypt + } +} + +impl std::fmt::Display for PinussyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for PinussyError {} + +impl ResponseError for PinussyError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(self.status_code()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..591d0c6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,26 @@ +#[macro_use] +mod actix_ructe; + +mod error; +mod notification; +pub mod routes; +mod users; + +use sqlx::{Pool, Postgres}; + +type Result = std::result::Result; + +#[derive(Clone)] +pub struct Pinussy { + pub db: Pool, +} + +#[derive(sqlx::Type)] +#[sqlx(type_name = "privacy", rename_all = "lowercase")] +pub enum Privacy { + Private, + Unlisted, + Public, +} + +include!(concat!(env!("OUT_DIR"), "/templates.rs")); diff --git a/src/main.rs b/src/main.rs index b10d0cf..6a439ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,33 +1,13 @@ -#[macro_use] -mod actix_ructe; - -use std::time::{Duration, SystemTime}; - use actix_session::storage::CookieSessionStore; -use actix_session::{Session, SessionGetError, SessionInsertError, SessionMiddleware}; -use actix_web::body::{BoxBody, EitherBody, MessageBody}; -use actix_web::cookie::time::Error; +use actix_session::SessionMiddleware; use actix_web::cookie::Key; -use actix_web::dev::ServiceResponse; -use actix_web::http::header::LOCATION; -use actix_web::http::{header, StatusCode}; -use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; -use actix_web::web::Redirect; -use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, ResponseError}; -use bcrypt::{hash, verify, DEFAULT_COST}; -use serde::Deserialize; -use sqlx::postgres::PgDatabaseError; -use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; -use templates::statics::StaticFile; - -static FAR: Duration = Duration::from_secs(180 * 24 * 60 * 60); - -type Result = std::result::Result; +use actix_web::http::StatusCode; +use actix_web::middleware::ErrorHandlers; +use actix_web::{web, App, HttpServer}; +use sqlx::postgres::PgPoolOptions; -#[derive(Clone)] -struct Pinussy { - db: Pool, -} +use pinussy::routes; +use pinussy::Pinussy; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -50,19 +30,19 @@ async fn main() -> std::io::Result<()> { )) .wrap( ErrorHandlers::new() - .handler(StatusCode::NOT_FOUND, render_404) - .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), + .handler(StatusCode::NOT_FOUND, routes::error::render_404) + .handler(StatusCode::INTERNAL_SERVER_ERROR, routes::error::render_500), ) .app_data(web::Data::new(pinussy.clone())) - .service(web::resource("/static/{filename}").to(static_file)) - .service(home) + .service(web::resource("/static/{filename}").to(routes::r#static::file)) + .service(routes::home::get) // .service(home_auth) - .service(get_login) - .service(post_login) - .service(post_logout) - .service(get_signup) - .service(post_signup) - .service(get_users) + .service(routes::login::get) + .service(routes::login::post) + .service(routes::logout::post) + .service(routes::signup::get) + .service(routes::signup::post) + .service(routes::users::get) // .service(get_pins) // .service(post_pin) // .service(get_pin) @@ -73,320 +53,3 @@ async fn main() -> std::io::Result<()> { .run() .await } - -#[get("/")] -async fn home(session: Session, state: web::Data) -> Result { - let username: Option; - if let Some(user_id) = session.get::("user_id")? { - username = Some( - sqlx::query!("select username from users where id = $1", user_id) - .fetch_one(&state.db) - .await? - .username, - ) - } else { - username = None - } - return Ok(HttpResponse::Ok().body(render!(templates::home_html, username).unwrap())); -} - -#[get("/signup")] -async fn get_signup() -> HttpResponse { - HttpResponse::Ok().body(render!(templates::signup_html, None).unwrap()) -} - -#[derive(Deserialize)] -struct SignupForm { - username: String, - password: String, -} - -#[post("/signup")] -async fn post_signup( - state: web::Data, - form: web::Form, -) -> Result { - let password_hash = hash(&form.password, DEFAULT_COST)?; - match sqlx::query!( - "insert into users(username, password) values ($1, $2)", - &form.username, - password_hash - ) - .execute(&state.db) - .await - { - Ok(_) => { - return Ok(HttpResponse::Ok().body( - render!( - templates::signup_html, - Some(Notification { - kind: NotificationKind::Info, - message: format!("you have successfully registered as {}", &form.username) - }) - ) - .unwrap(), - )) - } - Err(e) => { - match e { - sqlx::Error::Database(e) => { - if e.is_unique_violation() { - return Ok(HttpResponse::Conflict().body( - render!( - templates::signup_html, - Some(Notification { - kind: NotificationKind::Error, - message: format!( - "error: the username \"{}\" already exists", - &form.username - ) - }) - ) - .unwrap(), - )); - } - } - // TODO: log error - _ => {} - } - return Ok(HttpResponse::InternalServerError().body( - render!( - templates::signup_html, - Some(Notification { - kind: NotificationKind::Error, - message: "there was an internal server error. please try again later." - .to_owned() - }) - ) - .unwrap(), - )); - } - }; -} - -#[get("/login")] -async fn get_login() -> HttpResponse { - HttpResponse::Ok().body(render!(templates::login_html, None).unwrap()) -} - -#[derive(Deserialize)] -struct LoginForm { - username: String, - password: String, - rememberme: Option, -} - -#[post("/login")] -async fn post_login( - state: web::Data, - session: Session, - form: web::Form, -) -> Result { - match sqlx::query!( - "select id, password from users where username = $1", - &form.username - ) - .fetch_one(&state.db) - .await - { - Ok(user) => { - let password_hash: String = user.password; - if verify(&form.password, &password_hash)? { - session.insert("user_id", user.id)?; - return Ok(HttpResponse::SeeOther() - .insert_header((LOCATION, "/")) - .finish()); - } else { - return Ok(HttpResponse::Unauthorized().body( - render!( - templates::login_html, - Some(Notification { - kind: NotificationKind::Error, - message: "that password is incorrect".to_owned() - }) - ) - .unwrap(), - )); - } - } - Err(sqlx::Error::RowNotFound) => { - return Ok(HttpResponse::NotFound().body( - render!( - templates::login_html, - Some(Notification { - kind: NotificationKind::Error, - message: format!("the user \"{}\" does not exist", &form.username) - }) - ) - .unwrap(), - )); - } - Err(_) => { - return Ok(HttpResponse::InternalServerError().body( - render!( - templates::login_html, - Some(Notification { - kind: NotificationKind::Error, - message: "internal server error. please try again later".to_owned() - }) - ) - .unwrap(), - )); - } - } -} - -#[post("/logout")] -async fn post_logout(session: Session) -> HttpResponse { - session.purge(); - HttpResponse::SeeOther() - .insert_header((LOCATION, "/")) - .finish() -} - -#[derive(sqlx::Type)] -#[sqlx(type_name = "privacy", rename_all = "lowercase")] -enum Privacy { - Private, - Unlisted, - Public, -} - -#[derive(sqlx::FromRow)] -pub struct User { - id: i32, - username: String, - password: String, - email: Option, - bio: Option, - site: Option, - privacy: Privacy, - admin: bool, -} - -pub enum NotificationKind { - Info, - Warning, - Error, -} - -impl std::fmt::Display for NotificationKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - NotificationKind::Info => f.write_str("info"), - NotificationKind::Warning => f.write_str("warning"), - NotificationKind::Error => f.write_str("error"), - } - } -} - -pub struct Notification { - kind: NotificationKind, - message: String, -} - -#[get("/users")] -async fn get_users(state: web::Data) -> Result { - let users: Vec = sqlx::query_as("select * from users") - .fetch_all(&state.db) - .await - // TODO: no unwrap - .unwrap(); - println!("lol"); - Ok(HttpResponse::Ok().body(render!(templates::users_html, users).unwrap())) -} - -async fn static_file(path: web::Path) -> HttpResponse { - let name = &path.into_inner(); - if let Some(data) = StaticFile::get(name) { - let far_expires = SystemTime::now() + FAR; - HttpResponse::Ok() - .insert_header(header::Expires(far_expires.into())) - .insert_header(header::ContentType(data.mime.clone())) - .body(data.content) - } else { - HttpResponse::NotFound() - .reason("No such static file.") - .finish() - } -} - -fn render_404(res: ServiceResponse) -> actix_web::Result> { - Ok(error_response( - res, - StatusCode::NOT_FOUND, - "The resource you requested can't be found.", - )) -} - -fn render_500(res: ServiceResponse) -> actix_web::Result> { - Ok(error_response( - res, - StatusCode::INTERNAL_SERVER_ERROR, - "Sorry, Something went wrong. This is probably not your fault.", - )) -} - -fn error_response( - mut res: ServiceResponse, - status_code: StatusCode, - message: &str, -) -> ErrorHandlerResponse { - res.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()), - ); - ErrorHandlerResponse::Response(res.map_body(|_head, _body| { - EitherBody::right(MessageBody::boxed( - render!(templates::error_html, status_code, message).unwrap(), - )) - })) -} - -#[derive(Debug)] -enum PinussyError { - Database(sqlx::Error), - SessionInsertError, - SessionGetError, - Bcrypt, -} - -impl From for PinussyError { - fn from(_: SessionInsertError) -> Self { - Self::SessionInsertError - } -} - -impl From for PinussyError { - fn from(_: SessionGetError) -> Self { - Self::SessionGetError - } -} - -impl From for PinussyError { - fn from(e: sqlx::Error) -> Self { - Self::Database(e) - } -} - -impl From for PinussyError { - fn from(_: bcrypt::BcryptError) -> Self { - Self::Bcrypt - } -} - -impl std::fmt::Display for PinussyError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -impl std::error::Error for PinussyError {} - -impl ResponseError for PinussyError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(self.status_code()) - } -} - -include!(concat!(env!("OUT_DIR"), "/templates.rs")); diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..2283761 --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,20 @@ +pub enum Kind { + Info, + Warning, + Error, +} + +impl std::fmt::Display for Kind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Kind::Info => f.write_str("info"), + Kind::Warning => f.write_str("warning"), + Kind::Error => f.write_str("error"), + } + } +} + +pub struct Notification { + pub kind: Kind, + pub message: String, +} diff --git a/src/routes/error.rs b/src/routes/error.rs new file mode 100644 index 0000000..543b8a8 --- /dev/null +++ b/src/routes/error.rs @@ -0,0 +1,40 @@ +use actix_web::{ + body::{BoxBody, EitherBody, MessageBody}, + dev::ServiceResponse, + http::{header, StatusCode}, + middleware::ErrorHandlerResponse, +}; + +use crate::templates; + +pub fn render_404(res: ServiceResponse) -> actix_web::Result> { + Ok(error_response( + res, + StatusCode::NOT_FOUND, + "The resource you requested can't be found.", + )) +} + +pub fn render_500(res: ServiceResponse) -> actix_web::Result> { + Ok(error_response( + res, + StatusCode::INTERNAL_SERVER_ERROR, + "Sorry, Something went wrong. This is probably not your fault.", + )) +} + +fn error_response( + mut res: ServiceResponse, + status_code: StatusCode, + message: &str, +) -> ErrorHandlerResponse { + res.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()), + ); + ErrorHandlerResponse::Response(res.map_body(|_head, _body| { + EitherBody::right(MessageBody::boxed( + render!(templates::error_html, status_code, message).unwrap(), + )) + })) +} diff --git a/src/routes/home.rs b/src/routes/home.rs new file mode 100644 index 0000000..11d3a72 --- /dev/null +++ b/src/routes/home.rs @@ -0,0 +1,21 @@ +use actix_session::Session; +use actix_web::{get, web, HttpResponse}; + +use crate::templates; +use crate::{Pinussy, Result}; + +#[get("/")] +async fn get(session: Session, state: web::Data) -> Result { + let username: Option; + if let Some(user_id) = session.get::("user_id")? { + username = Some( + sqlx::query!("select username from users where id = $1", user_id) + .fetch_one(&state.db) + .await? + .username, + ) + } else { + username = None + } + return Ok(HttpResponse::Ok().body(render!(templates::home_html, username).unwrap())); +} diff --git a/src/routes/login.rs b/src/routes/login.rs new file mode 100644 index 0000000..33f7f69 --- /dev/null +++ b/src/routes/login.rs @@ -0,0 +1,82 @@ +use actix_session::Session; +use actix_web::http::header::LOCATION; +use actix_web::{get, post, web, HttpResponse}; +use bcrypt::verify; +use serde::Deserialize; + +use crate::notification::{Kind, Notification}; +use crate::templates; +use crate::Pinussy; +use crate::Result; + +#[get("/login")] +async fn get() -> HttpResponse { + HttpResponse::Ok().body(render!(templates::login_html, None).unwrap()) +} + +#[derive(Deserialize)] +struct LoginForm { + username: String, + password: String, + rememberme: Option, +} + +#[post("/login")] +async fn post( + state: web::Data, + session: Session, + form: web::Form, +) -> Result { + match sqlx::query!( + "select id, password from users where username = $1", + &form.username + ) + .fetch_one(&state.db) + .await + { + Ok(user) => { + let password_hash: String = user.password; + if verify(&form.password, &password_hash)? { + session.insert("user_id", user.id)?; + return Ok(HttpResponse::SeeOther() + .insert_header((LOCATION, "/")) + .finish()); + } else { + return Ok(HttpResponse::Unauthorized().body( + render!( + templates::login_html, + Some(Notification { + kind: Kind::Error, + message: "that password is incorrect".to_owned() + }) + ) + .unwrap(), + )); + } + } + Err(sqlx::Error::RowNotFound) => { + return Ok(HttpResponse::NotFound().body( + render!( + templates::login_html, + Some(Notification { + kind: Kind::Error, + message: format!("the user \"{}\" does not exist", &form.username) + }) + ) + .unwrap(), + )); + } + Err(_) => { + return Ok(HttpResponse::InternalServerError().body( + render!( + templates::login_html, + Some(Notification { + kind: Kind::Error, + message: "internal server error. please try again later".to_owned() + }) + ) + .unwrap(), + )); + } + } +} diff --git a/src/routes/logout.rs b/src/routes/logout.rs new file mode 100644 index 0000000..712b74c --- /dev/null +++ b/src/routes/logout.rs @@ -0,0 +1,11 @@ +use actix_session::Session; +use actix_web::http::header::LOCATION; +use actix_web::{post, HttpResponse}; + +#[post("/logout")] +async fn post(session: Session) -> HttpResponse { + session.purge(); + HttpResponse::SeeOther() + .insert_header((LOCATION, "/")) + .finish() +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..3b73f38 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,7 @@ +pub mod error; +pub mod home; +pub mod login; +pub mod logout; +pub mod signup; +pub mod r#static; +pub mod users; diff --git a/src/routes/signup.rs b/src/routes/signup.rs new file mode 100644 index 0000000..ae10201 --- /dev/null +++ b/src/routes/signup.rs @@ -0,0 +1,79 @@ +use actix_web::{get, post, web, HttpResponse}; +use serde::Deserialize; + +use crate::notification::{Kind as NotificationKind, Notification}; +use crate::templates; +use crate::Pinussy; +use crate::Result; +use bcrypt::{hash, DEFAULT_COST}; + +#[get("/signup")] +async fn get() -> HttpResponse { + HttpResponse::Ok().body(render!(templates::signup_html, None).unwrap()) +} + +#[derive(Deserialize)] +struct SignupForm { + username: String, + password: String, +} + +#[post("/signup")] +async fn post(state: web::Data, form: web::Form) -> Result { + let password_hash = hash(&form.password, DEFAULT_COST)?; + match sqlx::query!( + "insert into users(username, password) values ($1, $2)", + &form.username, + password_hash + ) + .execute(&state.db) + .await + { + Ok(_) => { + return Ok(HttpResponse::Ok().body( + render!( + templates::signup_html, + Some(Notification { + kind: NotificationKind::Info, + message: format!("you have successfully registered as {}", &form.username) + }) + ) + .unwrap(), + )) + } + Err(e) => { + match e { + sqlx::Error::Database(e) => { + if e.is_unique_violation() { + return Ok(HttpResponse::Conflict().body( + render!( + templates::signup_html, + Some(Notification { + kind: NotificationKind::Error, + message: format!( + "error: the username \"{}\" already exists", + &form.username + ) + }) + ) + .unwrap(), + )); + } + } + // TODO: log error + _ => {} + } + return Ok(HttpResponse::InternalServerError().body( + render!( + templates::signup_html, + Some(Notification { + kind: NotificationKind::Error, + message: "there was an internal server error. please try again later." + .to_owned() + }) + ) + .unwrap(), + )); + } + }; +} diff --git a/src/routes/static.rs b/src/routes/static.rs new file mode 100644 index 0000000..cabc6f8 --- /dev/null +++ b/src/routes/static.rs @@ -0,0 +1,22 @@ +use std::time::{Duration, SystemTime}; + +use actix_web::{http::header, web, HttpResponse}; + +use crate::templates::statics::StaticFile; + +static FAR: Duration = Duration::from_secs(180 * 24 * 60 * 60); + +pub async fn file(path: web::Path) -> HttpResponse { + let name = &path.into_inner(); + if let Some(data) = StaticFile::get(name) { + let far_expires = SystemTime::now() + FAR; + HttpResponse::Ok() + .insert_header(header::Expires(far_expires.into())) + .insert_header(header::ContentType(data.mime.clone())) + .body(data.content) + } else { + HttpResponse::NotFound() + .reason("No such static file.") + .finish() + } +} diff --git a/src/routes/users.rs b/src/routes/users.rs new file mode 100644 index 0000000..2ad9ede --- /dev/null +++ b/src/routes/users.rs @@ -0,0 +1,16 @@ +use actix_web::{get, web, HttpResponse}; + +use crate::templates; +use crate::users::User; +use crate::{Pinussy, Result}; + +#[get("/users")] +async fn get(state: web::Data) -> Result { + let users: Vec = sqlx::query_as("select * from users") + .fetch_all(&state.db) + .await + // TODO: no unwrap + .unwrap(); + println!("lol"); + Ok(HttpResponse::Ok().body(render!(templates::users_html, users).unwrap())) +} diff --git a/src/users.rs b/src/users.rs new file mode 100644 index 0000000..bf41fd5 --- /dev/null +++ b/src/users.rs @@ -0,0 +1,13 @@ +use crate::Privacy; + +#[derive(sqlx::FromRow)] +pub struct User { + pub id: i32, + pub username: String, + pub password: String, + pub email: Option, + pub bio: Option, + pub site: Option, + pub privacy: Privacy, + pub admin: bool, +} diff --git a/templates/base.rs.html b/templates/base.rs.html index 0bf70eb..434eb47 100644 --- a/templates/base.rs.html +++ b/templates/base.rs.html @@ -1,5 +1,5 @@ @use super::statics::*; -@use crate::Notification; +@use crate::notification::Notification; @(user: Option, notification: Option, body: Content) diff --git a/templates/home.rs.html b/templates/home.rs.html index 882b2ed..6726646 100644 --- a/templates/home.rs.html +++ b/templates/home.rs.html @@ -1,5 +1,4 @@ @use super::base_html; -@use crate::User; @(user: Option) diff --git a/templates/login.rs.html b/templates/login.rs.html index 8547bd0..10efaf3 100644 --- a/templates/login.rs.html +++ b/templates/login.rs.html @@ -1,5 +1,5 @@ @use super::base_html; -@use crate::Notification; +@use crate::notification::Notification; @(notification: Option) diff --git a/templates/signup.rs.html b/templates/signup.rs.html index 4823698..179ec98 100644 --- a/templates/signup.rs.html +++ b/templates/signup.rs.html @@ -1,5 +1,5 @@ @use super::base_html; -@use crate::Notification; +@use crate::notification::Notification; @(notification: Option) diff --git a/templates/users.rs.html b/templates/users.rs.html index c5b98f9..b1a7b8d 100644 --- a/templates/users.rs.html +++ b/templates/users.rs.html @@ -1,5 +1,5 @@ @use super::base_html; -@use crate::User; +@use crate::users::User; @(users: Vec) -- cgit