FrostByte/server/src/routes/post.rs

194 lines
5.6 KiB
Rust
Executable file

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<PostQueryParams>,
state: Data<ServerState>,
) -> Result<impl Responder> {
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<NewPost>,
state: Data<ServerState>,
auth: Data<Authentication>,
) -> Result<impl Responder> {
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<i64>,
state: Data<ServerState>,
auth: Data<Authentication>,
req: HttpRequest,
) -> Result<impl Responder> {
// 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<i64>,
state: Data<ServerState>,
auth: Data<Authentication>,
req: HttpRequest,
) -> Result<impl Responder> {
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<i64>, state: Data<ServerState>) -> Result<impl Responder> {
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<i64>, state: Data<ServerState>) -> Result<impl Responder> {
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")),
}
}