From 5d395d4ed73061b247c32dc63db6ddfa2dd62d39 Mon Sep 17 00:00:00 2001 From: cel 🌸 Date: Wed, 4 Oct 2023 19:55:17 +0100 Subject: initial commit --- src/main.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/main.rs (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..eb7f0e8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,221 @@ +#[macro_use] +mod actix_ructe; + +use std::time::{Duration, SystemTime}; + +use actix_web::body::{BoxBody, EitherBody, MessageBody}; +use actix_web::dev::ServiceResponse; +use actix_web::http::{header, StatusCode}; +use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; +use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, ResponseError}; +use bcrypt::{hash, verify, DEFAULT_COST}; +use serde::Deserialize; +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; + +#[derive(Clone)] +struct Pinussy { + db: Pool, +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let pool = PgPoolOptions::new() + .max_connections(5) + .connect("postgres://pinussy:pinussy@localhost/pinussy") + .await + .unwrap(); + + sqlx::migrate!("./migrations").run(&pool).await.unwrap(); + + let pinussy = Pinussy { db: pool }; + + HttpServer::new(move || { + App::new() + .wrap( + ErrorHandlers::new() + .handler(StatusCode::NOT_FOUND, render_404) + .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), + ) + .app_data(web::Data::new(pinussy.clone())) + .service(web::resource("/static/{filename}").to(static_file)) + .service(home) + .service(get_login) + .service(post_login) + .service(get_signup) + .service(post_signup) + .service(get_users) + // .service(get_pins) + // .service(post_pin) + // .service(get_pin) + // .service(post_board) + // .service(get_board) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await +} + +#[get("/")] +async fn home() -> HttpResponse { + HttpResponse::Ok().body("Hello world!") +} + +#[get("/signup")] +async fn get_signup() -> HttpResponse { + HttpResponse::Ok().body(render!(templates::signup_html).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)?; + sqlx::query!( + "insert into users(username, password) values ($1, $2)", + &form.username, + password_hash + ) + .execute(&state.db) + .await?; + Ok(HttpResponse::Ok().body(render!(templates::signup_html).unwrap())) +} + +#[get("/login")] +async fn get_login() -> HttpResponse { + HttpResponse::Ok().body(render!(templates::login_html).unwrap()) +} + +#[derive(Deserialize)] +struct LoginForm { + username: String, + password: String, + rememberme: bool, +} + +#[post("/login")] +async fn post_login(form: web::Form) -> Result { + Ok(HttpResponse::Ok().body(render!(templates::login_html).unwrap())) +} + +#[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, +} + +#[get("/users")] +async fn get_users(state: web::Data) -> Result { + let users: Vec = sqlx::query_as("select * from users") + .fetch_all(&state.db) + .await + .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), + Bcrypt, +} + +impl From for PinussyError { + fn from(e: sqlx::Error) -> Self { + Self::Database(e) + } +} + +impl From for PinussyError { + fn from(e: 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")); -- cgit