use crate::db::{db_get_latest_posts, db_get_post, db_new_post}; use crate::jwt::Authentication; use crate::types::{NewPost, PostQueryParams}; use crate::ServerState; use actix_web::web::{Data, Path, Query}; use actix_web::{delete, HttpRequest}; use actix_web::{get, post, web::Json, HttpResponse, Responder, Result}; use log::info; /// Gets all posts from the database, query parameters are optional /// If limit is not specified, it defaults to a sane value #[get("/posts")] pub async fn get_posts( query: Query, state: Data, ) -> Result { if let (Some(lim), Some(ofs)) = (query.limit, query.offset) { return Ok(HttpResponse::Ok() .json(db_get_latest_posts(&state.pool, std::cmp::min(lim, 30), ofs).await)); } Ok(HttpResponse::Ok().json(db_get_latest_posts(&state.pool, 30, 0).await)) } /// Creates a new post, requires a token in release mode #[post("/posts")] pub async fn new_post( new_post: Json, state: Data, auth: Data, ) -> Result { let user_claims = auth.decode(&new_post.token); if let Err(e) = user_claims { info!("Error validating token: {}", e); return Ok(HttpResponse::BadRequest().json("Error")); } // Bail if the token is invalid let claims = user_claims.unwrap(); info!("User {:?} created a new post", &claims.sub); let content = new_post.content.clone(); let username = claims.sub.clone(); // This one is avoidable if we just store the user id in the token let userid = sqlx::query!("SELECT id FROM users WHERE username = $1", username) .fetch_one(&state.pool) .await .unwrap() .id; // By now we know that the token is valid, so we can create the post return match db_new_post(userid, &content, &state.pool).await { Some(post) => { info!("Created post {:?}", post.id); Ok(HttpResponse::Ok().json(post)) } None => Ok(HttpResponse::InternalServerError().json("Error")), }; } #[post("/posts/{id}/engage")] pub async fn engage_post( path: Path, state: Data, auth: Data, req: HttpRequest, ) -> Result { // Token from header let token = req .headers() .get("Authorization") .unwrap() .to_str() .unwrap(); // Remove the Bearer prefix let token = token.replace("Bearer ", ""); let claims = auth.decode(&token); if let Err(e) = claims { info!("Error validating token: {}", e); return Ok(HttpResponse::BadRequest().json("Error")); } let post_id = path.into_inner(); let username = claims.unwrap().sub; let q = sqlx::query!( "INSERT INTO engagements (post_id, user_id) VALUES ($1, (SELECT id FROM users WHERE username = $2))", post_id, username ).execute(&state.pool).await; match q { Ok(_) => (), Err(e) => { info!("Error engaging post: {}", e); return Ok(HttpResponse::InternalServerError().json("Error")); } } // Get engagement count let q = sqlx::query!( "SELECT COUNT(*) FROM engagements WHERE post_id = $1", post_id ) .fetch_one(&state.pool) .await; match q { Ok(count) => Ok(HttpResponse::Ok().json(count.count)), Err(e) => { info!("Error getting engagements: {}", e); Ok(HttpResponse::InternalServerError().json("Error")) } } } #[delete("/posts/{id}")] pub async fn delete_post( path: Path, state: Data, auth: Data, req: HttpRequest, ) -> Result { let post_id = path.into_inner(); let token = req .headers() .get("Authorization") .unwrap() .to_str() .unwrap(); // Remove the Bearer prefix let token = token.replace("Bearer ", ""); let claims = auth.decode(&token); if let Err(e) = claims { info!("Error validating token: {}", e); return Ok(HttpResponse::BadRequest().json("Error")); } let username = claims.unwrap().sub; let q = sqlx::query!( "DELETE FROM posts WHERE id = $1 AND user_id = (SELECT id FROM users WHERE username = $2)", post_id, username ) .execute(&state.pool) .await; match q { Ok(q) => { // Does this include cascading deletes? if q.rows_affected() == 1 { Ok(HttpResponse::Ok().json("Deleted")) } else { Ok(HttpResponse::Forbidden().json("Forbidden")) } } Err(e) => { info!("Error deleting post: {}", e); Ok(HttpResponse::InternalServerError().json("Error")) } } } #[get("/posts/{id}/engage")] pub async fn get_engagements(path: Path, state: Data) -> Result { let id = path.into_inner(); let q = sqlx::query!("SELECT COUNT(*) FROM engagements WHERE post_id = $1", id) .fetch_one(&state.pool) .await; match q { Ok(count) => Ok(HttpResponse::Ok().json(count.count)), Err(e) => { info!("Error getting engagements: {}", e); Ok(HttpResponse::InternalServerError().json("Error")) } } } #[get("posts/{id}")] pub async fn post_by_id(path: Path, state: Data) -> Result { let id = path.into_inner(); match db_get_post(id, &state.pool).await { Some(post) => Ok(HttpResponse::Ok().json(post)), None => Ok(HttpResponse::NotFound().json("Error")), } }