diff --git a/src/jwt.rs b/src/jwt.rs new file mode 100644 index 0000000..4f14dc4 --- /dev/null +++ b/src/jwt.rs @@ -0,0 +1,13 @@ +use jsonwebtoken::EncodingKey; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize, Debug, Hash)] +pub struct Claims { + pub id: i32, + pub username: String, +} + +pub fn get_jwt_secret() -> EncodingKey { + let secret = std::env::var("SAAPI_JWT_SECRET").unwrap_or("Sdb\\y5PP`,hmG+98".into()); + EncodingKey::from_secret(secret.as_bytes()) +} diff --git a/src/main.rs b/src/main.rs index e1c4aff..dd84c16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,21 @@ +mod events; +mod jwt; +mod user; + use axum::{ - extract::State, - http::StatusCode, - response::IntoResponse, routing::{get, post}, - Json, Router, + Router, }; -use jsonwebtoken::{EncodingKey, Header}; -use serde::{Deserialize, Serialize}; -use serde_json::json; +use jsonwebtoken::EncodingKey; use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; use std::net::SocketAddr; - -#[derive(Clone, Serialize, Deserialize, Debug, Hash)] -struct Claims { - id: i32, - username: String, -} +use user::{signin, signup}; #[derive(Clone)] -struct AppState { - db_pool: PgPool, - jwt_key: EncodingKey, +pub struct AppState { + pub db_pool: PgPool, + pub jwt_key: EncodingKey, } #[tokio::main] @@ -34,9 +28,9 @@ async fn main() { let db_url = std::env::var("DATABASE_URL") .expect("Set `DATABASE_URL` to the url of the postgres database."); - let jwt_secret = std::env::var("SAAPI_JWT_SECRET").unwrap_or("Sdb\\y5PP`,hmG+98".into()); + let jwt_key = jwt::get_jwt_secret(); - let pool = PgPoolOptions::new() + let db_pool = PgPoolOptions::new() .max_connections(50) .connect(&db_url) .await @@ -44,15 +38,10 @@ async fn main() { // build our application with a route let app = Router::new() - // `GET /` goes to `root` .route("/", get(root)) - // `POST /users` goes to `create_user` .route("/user/signup", post(signup)) .route("/user/signin", post(signin)) - .with_state(AppState { - db_pool: pool, - jwt_key: EncodingKey::from_secret(jwt_secret.as_bytes()), - }); + .with_state(AppState { db_pool, jwt_key }); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` @@ -68,125 +57,3 @@ async fn main() { async fn root() -> &'static str { "Hello, World!" } - -async fn signup( - State(app_state): State, - Json(signup): Json, -) -> impl IntoResponse { - // insert your application logic here - let pass_hash = sha256::digest(&*signup.password); - - let result = sqlx::query!( - r#" - INSERT INTO users (username, password) - VALUES ( $1, $2 ) - RETURNING id; - "#, - signup.username, - pass_hash.as_bytes(), - ) - .fetch_one(&app_state.db_pool) - .await; - - match result { - Ok(record) => (StatusCode::CREATED, Json(json!({"id": record.id}))), - Err(err) => { - // See if we get an error for a duplicate usename - if let sqlx::Error::Database(database_err) = &err { - if let Some(err_code) = database_err.code() { - if err_code == "23505" { - return ( - StatusCode::BAD_REQUEST, - Json(json!({ - "error": format!("username '{}' is taken.", signup.username) - })), - ); - } - } - } - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ - "error": format!("Unknown error signing up: {}", err) - })), - ) - } - } -} - -async fn signin( - State(app_state): State, - Json(signin): Json, -) -> impl IntoResponse { - let pass_hash = sha256::digest(&*signin.password); - - let result = sqlx::query!( - r#" - SELECT id, username - FROM users - WHERE username = $1 AND password = $2 - "#, - signin.username, - pass_hash.as_bytes(), - ) - .fetch_optional(&app_state.db_pool) - .await; - - match result { - Ok(Some(user)) => { - let claims = Claims { - id: user.id, - username: user.username, - }; - let token = match jsonwebtoken::encode(&Header::default(), &claims, &app_state.jwt_key) - { - Ok(token) => token, - Err(err) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ - "error": format!("Unknown error signing in when creating JWT: {}", err) - })), - ) - } - }; - - return (StatusCode::OK, Json(json!({ "data": token }))); - } - Ok(None) => { - return ( - StatusCode::UNAUTHORIZED, - Json(json!({ - "error": format!("Incorrect username or password") - })), - ) - } - Err(err) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ - "error": format!("Unknown error signing in: {}", err) - })), - ) - } - } -} - -#[derive(Clone, Debug, Deserialize)] -struct Signup { - username: String, - password: String, -} - -#[derive(Clone, Debug, Deserialize)] -struct Signin { - username: String, - password: String, -} - -// the output to our `create_user` handler -#[derive(Serialize, sqlx::FromRow)] -struct User { - id: u64, - username: String, -} diff --git a/src/user.rs b/src/user.rs index e8da059..a133ab6 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,9 +1,127 @@ -use rocket::Route; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use jsonwebtoken::Header; +use serde::{Deserialize, Serialize}; +use serde_json::json; -pub fn routes() -> Vec { - routes![ +use crate::{jwt::Claims, AppState}; - ] +#[derive(Clone, Debug, Deserialize)] +pub struct Signup { + username: String, + password: String, } +#[derive(Clone, Debug, Deserialize)] +pub struct Signin { + username: String, + password: String, +} +#[derive(Serialize, sqlx::FromRow)] +struct User { + id: u64, + username: String, +} + +pub async fn signup( + State(app_state): State, + Json(signup): Json, +) -> impl IntoResponse { + // insert your application logic here + let pass_hash = sha256::digest(&*signup.password); + + let result = sqlx::query!( + r#" + INSERT INTO users (username, password) + VALUES ( $1, $2 ) + RETURNING id; + "#, + signup.username, + pass_hash.as_bytes(), + ) + .fetch_one(&app_state.db_pool) + .await; + + match result { + Ok(record) => (StatusCode::CREATED, Json(json!({"id": record.id}))), + Err(err) => { + // See if we get an error for a duplicate usename + if let sqlx::Error::Database(database_err) = &err { + if let Some(err_code) = database_err.code() { + if err_code == "23505" { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "error": format!("username '{}' is taken.", signup.username) + })), + ); + } + } + } + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "error": format!("Unknown error signing up: {}", err) + })), + ) + } + } +} + +pub async fn signin( + State(app_state): State, + Json(signin): Json, +) -> impl IntoResponse { + let pass_hash = sha256::digest(&*signin.password); + + let result = sqlx::query!( + r#" + SELECT id, username + FROM users + WHERE username = $1 AND password = $2 + "#, + signin.username, + pass_hash.as_bytes(), + ) + .fetch_optional(&app_state.db_pool) + .await; + + match result { + Ok(Some(user)) => { + let claims = Claims { + id: user.id, + username: user.username, + }; + let token = match jsonwebtoken::encode(&Header::default(), &claims, &app_state.jwt_key) + { + Ok(token) => token, + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "error": format!("Unknown error signing in when creating JWT: {}", err) + })), + ) + } + }; + + return (StatusCode::OK, Json(json!({ "data": token }))); + } + Ok(None) => { + return ( + StatusCode::UNAUTHORIZED, + Json(json!({ + "error": format!("Incorrect username or password") + })), + ) + } + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "error": format!("Unknown error signing in: {}", err) + })), + ) + } + } +}