added auth to create_event endpoint

This commit is contained in:
Mitchell Marino 2023-04-12 22:41:02 -05:00
parent 912e20399f
commit 711c5a88fb
6 changed files with 126 additions and 27 deletions

View File

@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
tokio = { version = "1.27", features = ["full"] }
axum = "0.6"
axum-auth = { version = "0.4", features = ["auth-bearer"]}
sqlx = { version = "0.6", features = ["postgres", "runtime-tokio-rustls", "all-types"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@ -8,5 +8,6 @@ RUN cargo install --path .
FROM debian:buster-slim
COPY --from=build /usr/local/cargo/bin/school_app_api /usr/local/bin/school_app_api
ENV DATABASE_URL "postgres://school_app_api_user:school_app_api_pass@school_app_db/school_app_api"
CMD ["school_app_api"]

View File

@ -1,4 +1,11 @@
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use axum_auth::AuthBearer;
use jsonwebtoken::{Algorithm, Validation};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::{
@ -6,7 +13,7 @@ use sqlx::{
types::{chrono::NaiveDateTime, BigDecimal},
};
use crate::AppState;
use crate::{jwt::Claims, user::Role, AppState};
#[derive(
sqlx::Type, Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
@ -38,7 +45,6 @@ pub struct NewEventRequestEntry {
place: Option<String>,
#[serde(with = "serialize_big_decimal")]
price: BigDecimal,
created_by: Option<i32>,
}
#[derive(Clone, Serialize, Debug)]
@ -136,13 +142,20 @@ LIMIT
}
pub async fn create_event(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
Json(event_post_query): Json<NewEventRequestEntry>,
) -> impl IntoResponse {
let token_data = match handle_token(token, &app_state, Role::Teacher) {
Ok(value) => value,
Err(value) => return value,
};
let result = query!(
r#"
INSERT INTO events (title, description, time_start, time_end, event_type, points, place, price, created_by)
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9 )
RETURNING id
"#,
event_post_query.title,
event_post_query.description,
@ -152,11 +165,11 @@ pub async fn create_event(
event_post_query.points,
event_post_query.place,
event_post_query.price,
event_post_query.created_by,
).execute(&app_state.db_pool).await;
token_data.id,
).fetch_one(&app_state.db_pool).await;
match result {
Ok(_) => (StatusCode::OK, "").into_response(),
Ok(record) => (StatusCode::OK, Json(json!({ "data": record.id }))).into_response(),
Err(err) => (
StatusCode::BAD_REQUEST,
Json(json!({
@ -166,3 +179,35 @@ pub async fn create_event(
.into_response(),
}
}
fn handle_token(
token: String,
app_state: &AppState,
minimum_role: Role,
) -> Result<Claims, Response> {
let token_result = jsonwebtoken::decode::<Claims>(
&token,
&app_state.jwt_decode,
&Validation::new(Algorithm::HS256),
);
let token_data = match token_result {
Ok(token_data) => token_data.claims,
Err(err) => {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({ "error": format!("Failed to parse JWT: {}", err) })),
)
.into_response())
}
};
if token_data.role < minimum_role {
return Err((
StatusCode::UNAUTHORIZED,
Json(json!({
"error": format!("You must be at least a {:?} to do this", minimum_role)
})),
)
.into_response());
}
Ok(token_data)
}

View File

@ -1,13 +1,50 @@
use jsonwebtoken::EncodingKey;
use std::time::Duration;
use jsonwebtoken::{DecodingKey, EncodingKey};
use serde::{Deserialize, Serialize};
use sqlx::types::time::OffsetDateTime;
use crate::user::Role;
pub const JWT_LIFETIME: Duration = Duration::from_secs(60 * 60 * 24 * 7);
#[derive(Clone, Serialize, Deserialize, Debug, Hash)]
pub struct Claims {
#[serde(with = "jwt_numeric_date")]
pub exp: OffsetDateTime,
pub id: i32,
pub username: String,
pub role: Role,
}
pub fn get_jwt_secret() -> EncodingKey {
pub fn get_jwt_keys() -> (EncodingKey, DecodingKey) {
let secret = std::env::var("SAAPI_JWT_SECRET").unwrap_or("Sdb\\y5PP`,hmG+98".into());
EncodingKey::from_secret(secret.as_bytes())
(
EncodingKey::from_secret(secret.as_bytes()),
DecodingKey::from_secret(secret.as_bytes()),
)
}
mod jwt_numeric_date {
//! Custom serialization of OffsetDateTime to conform with the JWT spec (RFC 7519 section 2, "Numeric Date")
use serde::{self, Deserialize, Deserializer, Serializer};
use sqlx::types::time::OffsetDateTime;
/// Serializes an OffsetDateTime to a Unix timestamp (milliseconds since 1970/1/1T00:00:00T)
pub fn serialize<S>(date: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let timestamp = date.unix_timestamp();
serializer.serialize_i64(timestamp)
}
/// Attempts to deserialize an i64 and use as a Unix timestamp
pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
OffsetDateTime::from_unix_timestamp(i64::deserialize(deserializer)?)
.map_err(|_| serde::de::Error::custom("invalid Unix timestamp value"))
}
}

View File

@ -6,7 +6,7 @@ use axum::{
routing::{get, post},
Router,
};
use jsonwebtoken::EncodingKey;
use jsonwebtoken::{DecodingKey, EncodingKey};
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::net::SocketAddr;
@ -15,7 +15,8 @@ use user::{signin, signup};
#[derive(Clone)]
pub struct AppState {
pub db_pool: PgPool,
pub jwt_key: EncodingKey,
pub jwt_encode: EncodingKey,
pub jwt_decode: DecodingKey,
}
#[tokio::main]
@ -27,7 +28,7 @@ async fn main() {
let db_url = std::env::var("DATABASE_URL")
.expect("Set `DATABASE_URL` to the url of the postgres database.");
let jwt_key = jwt::get_jwt_secret();
let (jwt_encode, jwt_decode) = jwt::get_jwt_keys();
let db_pool = PgPoolOptions::new()
.max_connections(50)
@ -42,7 +43,11 @@ async fn main() {
.route("/user/signin", post(signin))
.route("/event", post(events::create_event))
.route("/event/preview", get(events::get_events_preview))
.with_state(AppState { db_pool, jwt_key });
.with_state(AppState {
db_pool,
jwt_encode,
jwt_decode,
});
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@ -2,8 +2,12 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use jsonwebtoken::Header;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::types::time::OffsetDateTime;
use crate::{jwt::Claims, AppState};
use crate::{
jwt::{Claims, JWT_LIFETIME},
AppState,
};
#[derive(Clone, Debug, Deserialize)]
pub struct Signup {
@ -88,7 +92,10 @@ pub async fn signin(
let result = sqlx::query!(
r#"
SELECT id, username
SELECT
id,
username,
role AS "role!: Role"
FROM users
WHERE username = $1 AND password = $2
"#,
@ -101,17 +108,20 @@ pub async fn signin(
match result {
Ok(Some(user)) => {
let claims = Claims {
exp: OffsetDateTime::now_utc() + JWT_LIFETIME,
id: user.id,
username: user.username,
role: user.role,
};
let token = match jsonwebtoken::encode(&Header::default(), &claims, &app_state.jwt_key)
{
let token =
match jsonwebtoken::encode(&Header::default(), &claims, &app_state.jwt_encode) {
Ok(token) => token,
Err(err) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error signing in when creating JWT: {}", err)
"error":
format!("Unknown error signing in when creating JWT: {}", err)
})),
)
}