Merge branch 'master' of git.silversoft.se:Imbus/FrostByte
This commit is contained in:
commit
9bfe92f0d6
24 changed files with 498 additions and 219 deletions
|
@ -1,4 +1,4 @@
|
|||
use crate::routes::{Post, User};
|
||||
use crate::types::{Comment, Post, User};
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, SaltString},
|
||||
Argon2, PasswordHasher, PasswordVerifier,
|
||||
|
@ -6,6 +6,50 @@ use argon2::{
|
|||
use log::{info, warn};
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub async fn db_new_comment(
|
||||
pool: &PgPool,
|
||||
parent_post_id: i64,
|
||||
parent_comment_id: Option<i64>,
|
||||
user_id: i64,
|
||||
content: &str,
|
||||
) -> bool {
|
||||
let insert_query = sqlx::query!(
|
||||
"INSERT INTO comments (parent_post_id, parent_comment_id, author_user_id, content) VALUES ($1, $2, $3, $4)",
|
||||
parent_post_id,
|
||||
parent_comment_id.unwrap_or(-1), // This is a bit of a hack
|
||||
user_id,
|
||||
content
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
if insert_query.is_err() {
|
||||
let s = insert_query.err().unwrap();
|
||||
warn!("Error inserting comment into database: {}", s);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn db_get_comments(
|
||||
pool: &PgPool,
|
||||
parent_post_id: i64,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Vec<Comment> {
|
||||
sqlx::query_as!(
|
||||
Comment,
|
||||
"SELECT * FROM comments WHERE parent_post_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
|
||||
parent_post_id,
|
||||
limit,
|
||||
offset
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Gets the latest posts from the database, ordered by created_at
|
||||
pub async fn db_get_latest_posts(pool: &PgPool, limit: i64, offset: i64) -> Vec<Post> {
|
||||
sqlx::query_as!(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use actix_cors::Cors;
|
||||
use actix_files::Files;
|
||||
use actix_web::middleware;
|
||||
use actix_web::web::Data;
|
||||
|
@ -8,9 +9,10 @@ mod db;
|
|||
mod jwt;
|
||||
mod routes;
|
||||
mod state;
|
||||
mod types;
|
||||
mod util;
|
||||
|
||||
use routes::{get_posts, login, new_post, post_by_id, register};
|
||||
use routes::{get_comments, get_posts, login, new_comment, new_post, post_by_id, register};
|
||||
use state::CaptchaState;
|
||||
use state::ServerState;
|
||||
use util::hex_string;
|
||||
|
@ -33,7 +35,13 @@ async fn main() -> std::io::Result<()> {
|
|||
|
||||
info!("Spinning up server on http://localhost:8080");
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default()
|
||||
.allowed_origin("https://shitpost.se")
|
||||
.allowed_methods(vec!["GET", "POST"])
|
||||
.max_age(3600);
|
||||
|
||||
App::new()
|
||||
.wrap(cors)
|
||||
.wrap(middleware::Compress::default())
|
||||
.wrap(middleware::Logger::default())
|
||||
.wrap(middleware::NormalizePath::trim())
|
||||
|
@ -41,6 +49,8 @@ async fn main() -> std::io::Result<()> {
|
|||
scope("/api")
|
||||
.service(get_posts)
|
||||
.service(new_post)
|
||||
.service(new_comment)
|
||||
.service(get_comments)
|
||||
.service(post_by_id)
|
||||
.service(login)
|
||||
.service(register)
|
||||
|
|
69
server/src/routes/comment.rs
Normal file
69
server/src/routes/comment.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use crate::db::{db_get_comments, db_new_comment};
|
||||
use crate::jwt::validate_token;
|
||||
use crate::types::{CommentQueryParams, NewComment};
|
||||
use crate::ServerState;
|
||||
|
||||
use actix_web::get;
|
||||
use actix_web::web::{Data, Query};
|
||||
use actix_web::{post, web::Json, HttpResponse, Responder, Result};
|
||||
use log::info;
|
||||
|
||||
#[get("/comments")]
|
||||
pub async fn get_comments(
|
||||
comment_filter: Query<CommentQueryParams>,
|
||||
state: Data<ServerState>,
|
||||
) -> Result<impl Responder> {
|
||||
let post_id = comment_filter.post_id;
|
||||
let limit = comment_filter.limit.unwrap_or(10);
|
||||
let offset = comment_filter.offset.unwrap_or(0);
|
||||
|
||||
info!(
|
||||
"Getting comments for post {} with limit {} and offset {}",
|
||||
post_id, limit, offset
|
||||
);
|
||||
|
||||
let comments = db_get_comments(&state.pool, post_id, limit, offset).await;
|
||||
|
||||
Ok(HttpResponse::Ok().json(comments))
|
||||
}
|
||||
|
||||
#[post("/comments")]
|
||||
pub async fn new_comment(
|
||||
data: Json<NewComment>,
|
||||
state: Data<ServerState>,
|
||||
) -> Result<impl Responder> {
|
||||
let user_claims = validate_token(&data.user_token);
|
||||
|
||||
// Bail if the token is invalid
|
||||
if let Err(e) = user_claims {
|
||||
info!("Error validating token: {}", e);
|
||||
return Ok(HttpResponse::BadRequest().json("Error"));
|
||||
}
|
||||
|
||||
let claims = user_claims.unwrap();
|
||||
info!("User {:?} created a new comment", &claims.sub);
|
||||
|
||||
let content = data.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;
|
||||
|
||||
let success = db_new_comment(
|
||||
&state.pool,
|
||||
data.parent_post_id,
|
||||
data.parent_comment_id,
|
||||
userid,
|
||||
&content,
|
||||
)
|
||||
.await;
|
||||
|
||||
match success {
|
||||
true => Ok(HttpResponse::Ok().json("Success")),
|
||||
false => Ok(HttpResponse::BadRequest().json("Error")),
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
mod comment;
|
||||
mod post;
|
||||
mod users;
|
||||
|
||||
pub use comment::*;
|
||||
pub use post::*;
|
||||
pub use users::*;
|
||||
|
|
|
@ -1,57 +1,17 @@
|
|||
use crate::db::{db_get_latest_posts, db_get_post, db_new_post};
|
||||
use crate::jwt::validate_token;
|
||||
use crate::types::{NewPost, PostQueryParams};
|
||||
use crate::ServerState;
|
||||
|
||||
use actix_web::web::{Data, Path, Query};
|
||||
use actix_web::{get, post, web::Json, HttpResponse, Responder, Result};
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
// The post as it is received from the client
|
||||
// The token is used to identify the user
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NewPost {
|
||||
pub content: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
// The post as it is stored in the database, with all the related metadata
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, FromRow)]
|
||||
pub struct Post {
|
||||
pub id: i64,
|
||||
pub user_id: i64,
|
||||
pub content: String,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
/// The user as it is stored in the database, with all the related metadata
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, FromRow)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
// These look like /posts?limit=10&offset=20 in the URL
|
||||
// Note that these are optional
|
||||
/// Query parameters for the /posts endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct QueryParams {
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// 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<QueryParams>,
|
||||
query: Query<PostQueryParams>,
|
||||
state: Data<ServerState>,
|
||||
) -> Result<impl Responder> {
|
||||
if let (Some(lim), Some(ofs)) = (query.limit, query.offset) {
|
||||
|
|
|
@ -1,32 +1,13 @@
|
|||
use crate::db::{db_new_user, db_user_login};
|
||||
use crate::jwt::token_factory;
|
||||
use crate::state::CaptchaState;
|
||||
use crate::types::{AuthResponse, LoginData, RegisterData};
|
||||
use crate::ServerState;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{post, web::Json, HttpResponse, Responder, Result};
|
||||
use biosvg::BiosvgBuilder;
|
||||
use log::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginData {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthResponse {
|
||||
username: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterData {
|
||||
username: String,
|
||||
password: String,
|
||||
captcha: String,
|
||||
}
|
||||
|
||||
#[post("/register")]
|
||||
pub async fn register(
|
||||
|
@ -77,12 +58,6 @@ pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<im
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CaptchaResponse {
|
||||
captcha_svg: String,
|
||||
captcha_id: i32,
|
||||
}
|
||||
|
||||
/// Request a captcha from the captcha service
|
||||
#[allow(unreachable_code, unused_variables)]
|
||||
#[post("/captcha")]
|
||||
|
|
|
@ -43,7 +43,7 @@ impl ServerState {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::migrate!("./migrations_pg").run(&pool).await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
|
||||
|
||||
match crate::db::db_new_user("imbus".to_string(), "kartellen1234".to_string(), &pool).await
|
||||
{
|
||||
|
|
33
server/src/types/comment.rs
Normal file
33
server/src/types/comment.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The comment as it is received from the client
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NewComment {
|
||||
pub parent_post_id: i64,
|
||||
pub parent_comment_id: Option<i64>,
|
||||
pub user_token: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// The comment as it is stored in the database, with all the related metadata
|
||||
/// This is also the comment as it is sent to the client
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
|
||||
pub struct Comment {
|
||||
pub id: i64,
|
||||
pub parent_post_id: i64,
|
||||
pub parent_comment_id: Option<i64>,
|
||||
pub author_user_id: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
pub content: String,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
/// Query parameters for the /comments endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CommentQueryParams {
|
||||
pub post_id: i64,
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
7
server/src/types/mod.rs
Normal file
7
server/src/types/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod comment;
|
||||
mod post;
|
||||
mod user;
|
||||
|
||||
pub use comment::*;
|
||||
pub use post::*;
|
||||
pub use user::*;
|
31
server/src/types/post.rs
Normal file
31
server/src/types/post.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
// The post as it is received from the client
|
||||
// The token is used to identify the user
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NewPost {
|
||||
pub content: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
// The post as it is stored in the database, with all the related metadata
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, FromRow)]
|
||||
pub struct Post {
|
||||
pub id: i64,
|
||||
pub user_id: i64,
|
||||
pub content: String,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
// These look like /posts?limit=10&offset=20 in the URL
|
||||
// Note that these are optional
|
||||
/// Query parameters for the /posts endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PostQueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
42
server/src/types/user.rs
Normal file
42
server/src/types/user.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
/// The user as it is stored in the database, with all the related metadata
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, FromRow)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
/// The data recieved from the login form
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginData {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// The data recieved from the registration form
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterData {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub captcha: String,
|
||||
}
|
||||
|
||||
/// The response sent to the client after a successful login or registration
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthResponse {
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Data sent to the client to render the captcha
|
||||
/// The captcha_id is used to identify the captcha in the database
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CaptchaResponse {
|
||||
pub captcha_svg: String,
|
||||
pub captcha_id: i32,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue