diff --git a/server/src/jwt.rs b/server/src/jwt.rs index 63cb21a..5e61fd1 100755 --- a/server/src/jwt.rs +++ b/server/src/jwt.rs @@ -1,8 +1,12 @@ use jsonwebtoken::{ decode, encode, errors::Result as JwtResult, DecodingKey, EncodingKey, Header, Validation, }; +use log::*; use serde::{Deserialize, Serialize}; +const DAYS_VALID: i64 = 7; +const JWT_SECRET: &[u8] = "secret".as_bytes(); + /// Claims holds the data that will be encoded into the JWT token. #[derive(Debug, Serialize, Deserialize)] pub struct Claims { @@ -29,7 +33,6 @@ impl Claims { /// Authentication holds the data needed to encode and decode JWT tokens. // This is then passed to the AuthenticationMiddleware -#[derive(Clone)] pub struct Authentication { encoding_key: EncodingKey, decoding_key: DecodingKey, @@ -39,7 +42,7 @@ pub struct Authentication { impl Authentication { /// Create a new Authentication struct - pub fn new(secret: &[u8]) -> Self { + fn new(secret: &[u8]) -> Self { Authentication { encoding_key: EncodingKey::from_secret(secret), decoding_key: DecodingKey::from_secret(secret), @@ -54,18 +57,58 @@ impl Authentication { } /// Wrapper for encode_raw that takes a username (sub) and creates a Claims struct - pub fn encode(&self, sub: &str) -> JwtResult { + fn encode(&self, sub: &str) -> JwtResult { let claims = Claims::new(sub, self.days_valid); self.encode_raw(claims) } /// Decode a JWT token into a Claims struct // If this faie, it means the token is invalid - pub fn decode(&self, token: &str) -> JwtResult { + fn decode(&self, token: &str) -> JwtResult { decode::(token, &self.decoding_key, &self.validation).map(|data| data.claims) } } +// JwtResult is just a predefined error from the jsonwebtoken crate +pub fn token_factory(user: &str) -> JwtResult { + info!("Issuing JWT token for {}", user); + + let token = encode( + &Header::default(), + &Claims { + sub: user.to_string(), + iss: "frostbyte".to_string(), + iat: chrono::Utc::now().timestamp() as usize, + exp: (chrono::Utc::now() + chrono::Duration::days(DAYS_VALID)).timestamp() as usize, + }, + &EncodingKey::from_secret(JWT_SECRET), + ) + .unwrap(); + + Ok(token) +} + +// JwtResult is just a predefined error from the jsonwebtoken crate +// This function is incomplete and should be expanded to check for more things +pub fn validate_token(token: &str) -> JwtResult { + let token_data = decode::( + token, + &DecodingKey::from_secret(JWT_SECRET), + &Validation::default(), + ); + + match token_data { + Ok(token_data) => { + info!("Token validated for {}", token_data.claims.sub); + Ok(token_data.claims) + } + Err(e) => { + error!("Token validation failed: {}", e); + Err(e) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/server/src/main.rs b/server/src/main.rs index b3d0788..5e72109 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -12,7 +12,6 @@ mod state; mod types; mod util; -use jwt::Authentication; use routes::{get_comments, get_posts, login, new_comment, new_post, post_by_id, register}; use state::CaptchaState; use state::ServerState; @@ -24,7 +23,6 @@ async fn main() -> std::io::Result<()> { let data = ServerState::new().await; let capt_db = CaptchaState::new(); - let auth = Authentication::new("secret".as_bytes()); #[cfg(debug_assertions)] { @@ -39,13 +37,10 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { let cors = Cors::default() .allowed_origin("https://shitpost.se") + .allowed_origin("http://localhost:8080") .allowed_methods(vec!["GET", "POST"]) .max_age(3600); - // In debug mode, allow localhost - #[cfg(debug_assertions)] - let cors = cors.allowed_origin("http://localhost:8080"); - App::new() .wrap(cors) .wrap(middleware::Compress::default()) @@ -61,8 +56,7 @@ async fn main() -> std::io::Result<()> { .service(login) .service(register) .app_data(Data::new(data.clone())) - .app_data(Data::new(capt_db.clone())) - .app_data(Data::new(auth.clone())), + .app_data(Data::new(capt_db.clone())), ) .service( Files::new("/", "./public") diff --git a/server/src/routes/comment.rs b/server/src/routes/comment.rs index dcb1afe..3df7e00 100644 --- a/server/src/routes/comment.rs +++ b/server/src/routes/comment.rs @@ -1,5 +1,5 @@ use crate::db::{db_get_comments, db_new_comment}; -use crate::jwt::Authentication; +use crate::jwt::validate_token; use crate::types::{CommentQueryParams, NewComment}; use crate::ServerState; @@ -31,9 +31,8 @@ pub async fn get_comments( pub async fn new_comment( data: Json, state: Data, - auth: Data, ) -> Result { - let user_claims = auth.decode(&data.user_token); + let user_claims = validate_token(&data.user_token); // Bail if the token is invalid if let Err(e) = user_claims { diff --git a/server/src/routes/post.rs b/server/src/routes/post.rs index b54f49c..0eb4ec5 100755 --- a/server/src/routes/post.rs +++ b/server/src/routes/post.rs @@ -1,5 +1,5 @@ use crate::db::{db_get_latest_posts, db_get_post, db_new_post}; -use crate::jwt::Authentication; +use crate::jwt::validate_token; use crate::types::{NewPost, PostQueryParams}; use crate::ServerState; @@ -23,12 +23,8 @@ pub async fn get_posts( /// 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); +pub async fn new_post(new_post: Json, state: Data) -> Result { + let user_claims = validate_token(&new_post.token); if let Err(e) = user_claims { info!("Error validating token: {}", e); diff --git a/server/src/routes/users.rs b/server/src/routes/users.rs index 99f4edf..21013dd 100755 --- a/server/src/routes/users.rs +++ b/server/src/routes/users.rs @@ -1,6 +1,5 @@ use crate::db::{db_new_user, db_user_login}; -use crate::jwt::Authentication; -// use crate::jwt::token_factory; +use crate::jwt::token_factory; use crate::state::CaptchaState; use crate::types::{AuthResponse, LoginData, RegisterData}; use crate::ServerState; @@ -15,7 +14,6 @@ pub async fn register( data: Json, state: Data, captcha_state: Data, - auth: Data, ) -> Result { if !captcha_state .capthca_db @@ -32,7 +30,7 @@ pub async fn register( info!("User: {} registered", &user.username); Ok(HttpResponse::Ok().json(AuthResponse { username: user.username.clone(), - token: auth.encode(&user.username).unwrap(), + token: token_factory(&user.username).unwrap(), })) } None => { @@ -43,18 +41,14 @@ pub async fn register( } #[post("/login")] -pub async fn login( - data: Json, - state: Data, - auth: Data, -) -> Result { +pub async fn login(data: Json, state: Data) -> Result { let result = db_user_login(data.username.clone(), data.password.clone(), &state.pool).await; match result { Some(_) => { return Ok(HttpResponse::Ok().json(AuthResponse { username: data.username.clone(), - token: auth.encode(&data.username).unwrap(), + token: token_factory(&data.username).unwrap(), })); } None => { diff --git a/server/src/state.rs b/server/src/state.rs index dbbd254..01d1305 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -55,12 +55,9 @@ impl ServerState { None => error!("Failed to create default user..."), } - // We want dummy posts - lipsum_setup(&pool).await.unwrap(); - #[cfg(debug_assertions)] debug_setup(&pool).await.unwrap(); - + lipsum_setup(&pool).await.unwrap(); Self { pool } } } @@ -69,18 +66,48 @@ impl ServerState { // Mostly useful for debugging new posts, as we need to satisfy foreign key constraints. #[cfg(debug_assertions)] async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> { + use lipsum::lipsum; + use rand::prelude::*; + use sqlx::query; + use crate::db::db_new_user; + db_new_user("user".to_string(), "pass".to_string(), pool).await; + + // Check if the demo post already exists + let no_posts = query!("SELECT * FROM posts WHERE id = 1",) + .fetch_one(pool) + .await + .ok() + .is_none(); + + // If the demo user already has a post, don't insert another one + if no_posts { + let mut rng = rand::thread_rng(); + + // This requires that the user with id 1 exists in the user table + for _ in 0..100 { + query!( + "INSERT INTO posts (user_id, content) VALUES (1, $1)", + lipsum(rng.gen_range(10..100)) + ) + .execute(pool) + .await?; + } + + query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world! The demo username is user and the password is pass.')",) + .execute(pool) + .await?; + } + Ok(()) } -/// Inserts a bunch of dummy posts into the database async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { use lipsum::lipsum; use rand::prelude::*; use sqlx::query; - // Fetch any user let user_exist = query!("SELECT * FROM users",) .fetch_one(pool) .await @@ -90,7 +117,6 @@ async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { if user_exist { let mut rng = rand::thread_rng(); - // Insert a 100 dummy posts. // This requires that the user with id 1 exists in the user table for _ in 0..100 { query!( @@ -104,16 +130,3 @@ async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_server_state() { - let state = CaptchaState::new(); - assert!(state.capthca_db.lock().unwrap().is_empty()); - state.capthca_db.lock().unwrap().insert("test".to_string()); - assert!(!state.capthca_db.lock().unwrap().is_empty()); - } -}