summaryrefslogblamecommitdiffstats
path: root/src/main.rs
blob: eb7f0e8ddc1f778459ecc4235745030013b433f6 (plain) (tree)




























































































































































































































                                                                                         
#[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<T> = std::result::Result<T, PinussyError>;

#[derive(Clone)]
struct Pinussy {
    db: Pool<Postgres>,
}

#[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<Pinussy>,
    form: web::Form<SignupForm>,
) -> Result<HttpResponse> {
    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<LoginForm>) -> Result<HttpResponse> {
    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<String>,
    bio: Option<String>,
    site: Option<String>,
    privacy: Privacy,
    admin: bool,
}

#[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
        .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),
    Bcrypt,
}

impl From<sqlx::Error> for PinussyError {
    fn from(e: sqlx::Error) -> Self {
        Self::Database(e)
    }
}

impl From<bcrypt::BcryptError> 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<BoxBody> {
        HttpResponse::new(self.status_code())
    }
}

include!(concat!(env!("OUT_DIR"), "/templates.rs"));