#[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_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; #[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(SessionMiddleware::new( // TODO: postgres session store CookieSessionStore::default(), Key::generate(), )) .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(home_auth) .service(get_login) .service(post_login) .service(post_logout) .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(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"));