diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/error.rs | 48 | ||||
-rw-r--r-- | src/lib.rs | 26 | ||||
-rw-r--r-- | src/main.rs | 371 | ||||
-rw-r--r-- | src/notification.rs | 20 | ||||
-rw-r--r-- | src/routes/error.rs | 40 | ||||
-rw-r--r-- | src/routes/home.rs | 21 | ||||
-rw-r--r-- | src/routes/login.rs | 82 | ||||
-rw-r--r-- | src/routes/logout.rs | 11 | ||||
-rw-r--r-- | src/routes/mod.rs | 7 | ||||
-rw-r--r-- | src/routes/signup.rs | 79 | ||||
-rw-r--r-- | src/routes/static.rs | 22 | ||||
-rw-r--r-- | src/routes/users.rs | 16 | ||||
-rw-r--r-- | src/users.rs | 13 |
13 files changed, 402 insertions, 354 deletions
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<SessionInsertError> for PinussyError { + fn from(_: SessionInsertError) -> Self { + Self::SessionInsertError + } +} + +impl From<SessionGetError> for PinussyError { + fn from(_: SessionGetError) -> Self { + Self::SessionGetError + } +} + +impl From<sqlx::Error> for PinussyError { + fn from(e: sqlx::Error) -> Self { + Self::Database(e) + } +} + +impl From<bcrypt::BcryptError> 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<BoxBody> { + 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<T> = std::result::Result<T, crate::error::PinussyError>; + +#[derive(Clone)] +pub struct Pinussy { + pub db: Pool<Postgres>, +} + +#[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<T> = std::result::Result<T, PinussyError>; +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<Postgres>, -} +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<Pinussy>) -> Result<HttpResponse> { - let username: Option<String>; - if let Some(user_id) = session.get::<i32>("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<Pinussy>, - form: web::Form<SignupForm>, -) -> Result<HttpResponse> { - 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<String>, -} - -#[post("/login")] -async fn post_login( - state: web::Data<Pinussy>, - session: Session, - form: web::Form<SignupForm>, -) -> Result<HttpResponse> { - 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<String>, - bio: Option<String>, - site: Option<String>, - 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<Pinussy>) -> Result<HttpResponse> { - let users: Vec<User> = 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<String>) -> 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<ErrorHandlerResponse<BoxBody>> { - Ok(error_response( - res, - StatusCode::NOT_FOUND, - "The resource you requested can't be found.", - )) -} - -fn render_500(res: ServiceResponse) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> { - 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<BoxBody> { - 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<SessionInsertError> for PinussyError { - fn from(_: SessionInsertError) -> Self { - Self::SessionInsertError - } -} - -impl From<SessionGetError> for PinussyError { - fn from(_: SessionGetError) -> Self { - Self::SessionGetError - } -} - -impl From<sqlx::Error> for PinussyError { - fn from(e: sqlx::Error) -> Self { - Self::Database(e) - } -} - -impl From<bcrypt::BcryptError> 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<BoxBody> { - 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<ErrorHandlerResponse<BoxBody>> { + Ok(error_response( + res, + StatusCode::NOT_FOUND, + "The resource you requested can't be found.", + )) +} + +pub fn render_500(res: ServiceResponse) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> { + 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<BoxBody> { + 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<Pinussy>) -> Result<HttpResponse> { + let username: Option<String>; + if let Some(user_id) = session.get::<i32>("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<String>, +} + +#[post("/login")] +async fn post( + state: web::Data<Pinussy>, + session: Session, + form: web::Form<LoginForm>, +) -> Result<HttpResponse> { + 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<Pinussy>, form: web::Form<SignupForm>) -> Result<HttpResponse> { + 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<String>) -> 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<Pinussy>) -> Result<HttpResponse> { + let users: Vec<User> = 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<String>, + pub bio: Option<String>, + pub site: Option<String>, + pub privacy: Privacy, + pub admin: bool, +} |