added query for reports
This commit is contained in:
parent
a48cace929
commit
7842a6014c
@ -9,7 +9,7 @@ services:
|
||||
POSTGRES_USER: school_app_api_user
|
||||
PGDATA: /db
|
||||
volumes:
|
||||
- ./pgdata:/db:rw
|
||||
- /home/mitchell/prod/school_app_api/pgdata:/db:rw
|
||||
|
||||
school-app-api:
|
||||
image: registry.mdev.local/school_app_api
|
||||
|
||||
5
migrations/20230414021153_event_attending.sql
Normal file
5
migrations/20230414021153_event_attending.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- fill existing users with grade 9
|
||||
ALTER TABLE users ADD COLUMN grade INTEGER NOT NULL DEFAULT 9;
|
||||
ALTER TABLE users ALTER COLUMN grade DROP DEFAULT;
|
||||
|
||||
ALTER TABLE event_attendees ADD COLUMN confirmed BOOLEAN NOT NULL DEFAULT false;
|
||||
157
src/events.rs
157
src/events.rs
@ -1,107 +1,14 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use axum_auth::AuthBearer;
|
||||
use jsonwebtoken::{Algorithm, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::{
|
||||
query, query_as,
|
||||
types::{chrono::NaiveDateTime, BigDecimal},
|
||||
use sqlx::{query, query_as};
|
||||
|
||||
use crate::{
|
||||
jwt::handle_token,
|
||||
models::{Event, EventType, NewEventRequestEntry, Role},
|
||||
AppState,
|
||||
};
|
||||
|
||||
use crate::{jwt::Claims, user::Role, AppState};
|
||||
|
||||
#[derive(
|
||||
sqlx::Type, Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
#[sqlx(type_name = "event_type", rename_all = "snake_case")]
|
||||
enum EventType {
|
||||
Sports,
|
||||
Meetings,
|
||||
Drama,
|
||||
Music,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct EventsPreviewQuery {
|
||||
count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
pub struct NewEventRequestEntry {
|
||||
title: String,
|
||||
description: String,
|
||||
#[serde(with = "serialize_datetime")]
|
||||
time_start: NaiveDateTime,
|
||||
#[serde(with = "serialize_datetime")]
|
||||
time_end: NaiveDateTime,
|
||||
event_type: EventType,
|
||||
points: i32,
|
||||
place: Option<String>,
|
||||
#[serde(with = "serialize_big_decimal")]
|
||||
price: BigDecimal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Debug)]
|
||||
pub struct Event {
|
||||
id: i32,
|
||||
title: String,
|
||||
description: String,
|
||||
#[serde(with = "serialize_datetime")]
|
||||
time_start: NaiveDateTime,
|
||||
#[serde(with = "serialize_datetime")]
|
||||
time_end: NaiveDateTime,
|
||||
event_type: EventType,
|
||||
points: i32,
|
||||
place: Option<String>,
|
||||
#[serde(with = "serialize_big_decimal")]
|
||||
price: BigDecimal,
|
||||
created_by: Option<i32>,
|
||||
}
|
||||
|
||||
mod serialize_datetime {
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
|
||||
pub fn serialize<S>(dt: &NaiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&dt.to_string())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<NaiveDateTime, D::Error> {
|
||||
let str = <String as Deserialize>::deserialize(deserializer)?;
|
||||
NaiveDateTime::from_str(&str).map_err(|err| serde::de::Error::custom(err))
|
||||
}
|
||||
}
|
||||
|
||||
mod serialize_big_decimal {
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use sqlx::types::BigDecimal;
|
||||
|
||||
pub fn serialize<S>(bd: &BigDecimal, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&bd.to_string())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<BigDecimal, D::Error> {
|
||||
let float = f32::deserialize(deserializer)?;
|
||||
BigDecimal::try_from(float).map_err(|err| serde::de::Error::custom(err))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_events_preview(State(app_state): State<AppState>) -> impl IntoResponse {
|
||||
let result = query_as!(
|
||||
Event,
|
||||
@ -144,7 +51,7 @@ LIMIT
|
||||
pub async fn create_event(
|
||||
AuthBearer(token): AuthBearer,
|
||||
State(app_state): State<AppState>,
|
||||
Json(event_post_query): Json<NewEventRequestEntry>,
|
||||
Json(event_req): Json<NewEventRequestEntry>,
|
||||
) -> impl IntoResponse {
|
||||
let token_data = match handle_token(token, &app_state, Role::Teacher) {
|
||||
Ok(value) => value,
|
||||
@ -157,14 +64,14 @@ pub async fn create_event(
|
||||
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9 )
|
||||
RETURNING id
|
||||
"#,
|
||||
event_post_query.title,
|
||||
event_post_query.description,
|
||||
event_post_query.time_start,
|
||||
event_post_query.time_end,
|
||||
event_post_query.event_type as EventType,
|
||||
event_post_query.points,
|
||||
event_post_query.place,
|
||||
event_post_query.price,
|
||||
event_req.title,
|
||||
event_req.description,
|
||||
event_req.time_start,
|
||||
event_req.time_end,
|
||||
event_req.event_type as EventType,
|
||||
event_req.points,
|
||||
event_req.place,
|
||||
event_req.price,
|
||||
token_data.id,
|
||||
).fetch_one(&app_state.db_pool).await;
|
||||
|
||||
@ -179,35 +86,3 @@ 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)
|
||||
}
|
||||
|
||||
76
src/jwt.rs
76
src/jwt.rs
@ -1,22 +1,18 @@
|
||||
use crate::{
|
||||
models::{Claims, Role},
|
||||
AppState,
|
||||
};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation};
|
||||
use serde_json::json;
|
||||
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_keys() -> (EncodingKey, DecodingKey) {
|
||||
let secret = std::env::var("SAAPI_JWT_SECRET").unwrap_or("Sdb\\y5PP`,hmG+98".into());
|
||||
(
|
||||
@ -25,26 +21,34 @@ pub fn get_jwt_keys() -> (EncodingKey, DecodingKey) {
|
||||
)
|
||||
}
|
||||
|
||||
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"))
|
||||
pub 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)
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
mod events;
|
||||
mod jwt;
|
||||
mod models;
|
||||
mod report;
|
||||
mod user;
|
||||
|
||||
use axum::{
|
||||
|
||||
162
src/models.rs
Normal file
162
src/models.rs
Normal file
@ -0,0 +1,162 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::{chrono::NaiveDateTime, time::OffsetDateTime, BigDecimal};
|
||||
|
||||
/// The possible event types.
|
||||
#[derive(
|
||||
sqlx::Type, Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
#[sqlx(type_name = "event_type", rename_all = "snake_case")]
|
||||
pub enum EventType {
|
||||
Sports,
|
||||
Meetings,
|
||||
Drama,
|
||||
Music,
|
||||
Other,
|
||||
}
|
||||
|
||||
/// The model for the body of the create event request.
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
pub struct NewEventRequestEntry {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
#[serde(with = "serde_datetime")]
|
||||
pub time_start: NaiveDateTime,
|
||||
#[serde(with = "serde_datetime")]
|
||||
pub time_end: NaiveDateTime,
|
||||
pub event_type: EventType,
|
||||
pub points: i32,
|
||||
pub place: Option<String>,
|
||||
#[serde(with = "serde_big_decimal")]
|
||||
pub price: BigDecimal,
|
||||
}
|
||||
|
||||
/// The model for an Event in the db.
|
||||
#[derive(Clone, Serialize, Debug)]
|
||||
pub struct Event {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
#[serde(with = "serde_datetime")]
|
||||
pub time_start: NaiveDateTime,
|
||||
#[serde(with = "serde_datetime")]
|
||||
pub time_end: NaiveDateTime,
|
||||
pub event_type: EventType,
|
||||
pub points: i32,
|
||||
pub place: Option<String>,
|
||||
#[serde(with = "serde_big_decimal")]
|
||||
pub price: BigDecimal,
|
||||
pub created_by: Option<i32>,
|
||||
}
|
||||
|
||||
/// The model for the body of the signup request.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Signup {
|
||||
pub username: String,
|
||||
pub role: Role,
|
||||
pub grade: i32,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// The possible roles for a user.
|
||||
#[derive(
|
||||
sqlx::Type, Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
// #[sqlx(type_name = "role", rename_all = "snake_case")]
|
||||
pub enum Role {
|
||||
Student,
|
||||
Teacher,
|
||||
Admin,
|
||||
}
|
||||
|
||||
/// The model for the signin request.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Signin {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// The model for the query parameters of the report request.
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct ReportQuery {
|
||||
pub grade: Option<i32>,
|
||||
}
|
||||
|
||||
/// The claims for the JWT.
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Hash)]
|
||||
pub struct Claims {
|
||||
#[serde(with = "serde_numeric_date")]
|
||||
pub exp: OffsetDateTime,
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
sqlx::Type, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct UserReportEntry {
|
||||
pub username: String,
|
||||
pub points: i32,
|
||||
}
|
||||
|
||||
/// Module for (de)serializing [OffsetDateTime] to conform with the JWT spec (RFC 7519 section 2, "Numeric Date")
|
||||
mod serde_numeric_date {
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Module for (de)serializing [NaiveDateTime]
|
||||
mod serde_datetime {
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
|
||||
pub fn serialize<S>(dt: &NaiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&dt.to_string())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<NaiveDateTime, D::Error> {
|
||||
let str = <String as Deserialize>::deserialize(deserializer)?;
|
||||
NaiveDateTime::from_str(&str).map_err(|err| serde::de::Error::custom(err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Module for (de)serializing [BigDecimal]
|
||||
mod serde_big_decimal {
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use sqlx::types::BigDecimal;
|
||||
|
||||
pub fn serialize<S>(bd: &BigDecimal, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&bd.to_string())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<BigDecimal, D::Error> {
|
||||
let float = f32::deserialize(deserializer)?;
|
||||
BigDecimal::try_from(float).map_err(|err| serde::de::Error::custom(err))
|
||||
}
|
||||
}
|
||||
59
src/report.rs
Normal file
59
src/report.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use axum_auth::AuthBearer;
|
||||
use serde_json::json;
|
||||
use sqlx::{query, query_as};
|
||||
|
||||
use crate::{
|
||||
jwt::handle_token,
|
||||
models::{ReportQuery, Role, UserReportEntry},
|
||||
AppState,
|
||||
};
|
||||
|
||||
pub async fn report(
|
||||
AuthBearer(token): AuthBearer,
|
||||
State(app_state): State<AppState>,
|
||||
Query(report_query): Query<ReportQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let token_data = match handle_token(token, &app_state, Role::Teacher) {
|
||||
Ok(value) => value,
|
||||
Err(value) => return value,
|
||||
};
|
||||
|
||||
let result = query_as!(
|
||||
UserReportEntry,
|
||||
r#"
|
||||
SELECT
|
||||
u.username,
|
||||
COALESCE(SUM(e.points), 0) AS "points!: i32"
|
||||
FROM
|
||||
users u
|
||||
LEFT JOIN event_attendees ea
|
||||
ON u.id = ea.user_id AND ea.confirmed = true
|
||||
LEFT JOIN events e
|
||||
ON ea.event_id = e.id
|
||||
WHERE
|
||||
u.grade = $1 OR $2
|
||||
GROUP BY u.id;
|
||||
"#,
|
||||
report_query.grade.unwrap_or(0),
|
||||
report_query.grade.is_none(),
|
||||
)
|
||||
.fetch_all(&app_state.db_pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(record) => (StatusCode::OK, Json(json!({ "data": record }))).into_response(),
|
||||
Err(err) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": format!("Unknown error creating event: {}", err)
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
38
src/user.rs
38
src/user.rs
@ -1,43 +1,14 @@
|
||||
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, JWT_LIFETIME},
|
||||
jwt::JWT_LIFETIME,
|
||||
models::{Claims, Role, Signin, Signup},
|
||||
AppState,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Signup {
|
||||
username: String,
|
||||
role: Role,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
sqlx::Type, Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
#[sqlx(type_name = "role", rename_all = "snake_case")]
|
||||
pub enum Role {
|
||||
Student,
|
||||
Teacher,
|
||||
Admin,
|
||||
}
|
||||
|
||||
#[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<AppState>,
|
||||
Json(signup): Json<Signup>,
|
||||
@ -47,12 +18,13 @@ pub async fn signup(
|
||||
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO users (username, role, password)
|
||||
VALUES ( $1, $2, $3 )
|
||||
INSERT INTO users (username, role, grade, password)
|
||||
VALUES ( $1, $2, $3, $4 )
|
||||
RETURNING id;
|
||||
"#,
|
||||
signup.username,
|
||||
signup.role as Role,
|
||||
signup.grade,
|
||||
pass_hash.as_bytes(),
|
||||
)
|
||||
.fetch_one(&app_state.db_pool)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user