/** * Comments generator * POST /comment - submits a comment * * GET /form - generate html comment form * GET /comment - gets html fragment with comments * both accept a query parameter ?page= */ use askama::Template; use axum::{ extract::{Query, Form, State}, response::Redirect, http::StatusCode, routing::get, Router, }; use serde::Deserialize; use sqlx::{ postgres::{PgPool, PgPoolOptions}, types::{ time::OffsetDateTime, uuid::Uuid, }, }; use std::{net::SocketAddr, time::Duration}; // Useful global thingies #[derive(Clone)] struct Ctx { pool: PgPool, } #[tokio::main] async fn main() { let db_connection_str = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://postgres:password@localhost".to_string()); // setup connection pool let pool = PgPoolOptions::new() .max_connections(5) .connect_timeout(Duration::from_secs(3)) .connect(&db_connection_str) .await .expect("can't connect to database"); sqlx::migrate!() .run(&pool) .await .expect("failed to run migrations"); let ctx = Ctx {pool}; // build our application with some routes let app = Router::new() .route( "/form", get(get_form), ) .route( "/comment", get(get_comments).post(post_comments), ) .with_state(ctx); // run it with hyper let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } #[derive(Deserialize)] struct UrlQuery { url: String } #[derive(Template)] #[template(path = "form.html")] struct CommentForm { url: String, capcha_question: String, capcha_id: Uuid, } async fn get_form( State(ctx): State, Query(uq): Query ) -> Result { let capcha = sqlx::query!("select id, question from capchas order by random() limit 1") .fetch_one(&ctx.pool) .await .map_err(internal_error)?; let c = CommentForm{url: uq.url, capcha_question: capcha.question, capcha_id: capcha.id}; let res = c.render().map_err(internal_error)?; Ok(res) } #[derive(Template)] #[template(path = "comments.html")] struct Comments { comments: Vec, } struct Comment { author: String, comment: String, ts: OffsetDateTime, } async fn get_comments( State(ctx): State, Query(uq): Query) -> Result { let comments = sqlx::query!("select author,comment,ts from comments where url = $1", uq.url) .fetch_all(&ctx.pool) .await .map_err(internal_error)?; let render_comments: Vec = comments.into_iter().map(|comment| { Comment { author: comment.author, comment: comment.comment, ts: comment.ts, } }).collect(); let c = Comments{comments: render_comments}; let res = c.render().map_err(internal_error)?; Ok(res) } #[derive(Deserialize)] struct PostComment { url: String, author: String, comment: String, capcha_id: String, capcha_answer: String, } async fn post_comments( State(ctx): State, Form(post_comment): Form) -> Result { let capcha_id: Uuid = post_comment.capcha_id.parse() .map_err(|_| {(StatusCode::BAD_REQUEST, "Invalid capcha_id".to_string())})?; let ans: String = sqlx::query_as!("select answer from capchas where id = $1", capcha_id) .fetch_one(&ctx.pool) .await .map_err(internal_error)?; if post_comment.capcha_answer != ans { return Err((StatusCode::BAD_REQUEST, "Capcha was wrong!".to_string())); } sqlx::query!("insert into comments(url,author,comment) values($1, $2, $3)", post_comment.url, post_comment.author, post_comment.comment) .execute(&ctx.pool) .await .map_err(internal_error)?; Ok(Redirect::temporary(&post_comment.url)) } fn internal_error(err: E) -> (StatusCode, String) where E: std::error::Error, { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) }