Compare commits

..

No commits in common. "e0ce62ddb7aca1d07718e96bd5dce5e91e73deeb" and "2f8ddf075487bde42f8b2eaff9d836c535931f87" have entirely different histories.

11 changed files with 45 additions and 328 deletions

View File

@ -8,8 +8,6 @@ edition = "2021"
[dependencies]
tokio = { version = "1.27", features = ["full"] }
axum = "0.6"
tower = "0.4"
tower-http = { version = "0.4", features = ["cors"]}
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"] }

View File

@ -1,4 +0,0 @@
# EngageEarn API
The api for the EngageEarn app.

View File

@ -148,7 +148,6 @@ jobs:
- name: build
plan:
- get: docker-image
- get: repo
trigger: true
passed: [test]
@ -191,7 +190,6 @@ jobs:
repository: concourse/oci-build-task
inputs:
- name: docker-image
- name: repo
path: .

View File

@ -1,16 +1,11 @@
use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use axum_auth::AuthBearer;
use serde_json::json;
use sqlx::{query, query_as};
use sqlx::query;
use crate::{
jwt::handle_token,
models::{AttendingEntry, ConfirmAttending, GetAttendingQuery, MarkAttending, Role},
models::{ConfirmAttending, MarkAttending, Role},
AppState,
};
@ -62,44 +57,6 @@ pub async fn confirm_attending(
.into_response()
}
pub async fn get_attending(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
Query(attending_query): Query<GetAttendingQuery>,
) -> impl IntoResponse {
match handle_token(token, &app_state, Role::Teacher) {
Ok(token_data) => token_data,
Err(err) => return err,
};
let result = query_as!(
AttendingEntry,
r#"
SELECT
user_id,
u.username
FROM event_attendees ea
INNER JOIN users u
ON u.id = ea.user_id
WHERE event_id = $1
"#,
attending_query.event_id,
)
.fetch_all(&app_state.db_pool)
.await;
match result {
Ok(attending) => (StatusCode::OK, Json(json!(attending))),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error confirming attendence: {:?}", err)
})),
),
}
.into_response()
}
pub async fn mark_attending(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,

View File

@ -18,8 +18,9 @@ pub async fn get_events_preview(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
) -> impl IntoResponse {
if let Err(err) = handle_token(token, &app_state, Role::Student) {
return err;
match handle_token(token, &app_state, Role::Student) {
Ok(_) => {}
Err(err) => return err,
};
let result = query_as!(
@ -65,8 +66,9 @@ pub async fn get_all_events(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
) -> impl IntoResponse {
if let Err(err) = handle_token(token, &app_state, Role::Student) {
return err;
match handle_token(token, &app_state, Role::Student) {
Ok(_) => {}
Err(err) => return err,
};
let result = query_as!(
@ -108,55 +110,6 @@ pub async fn get_all_events(
.into_response()
}
pub async fn my_events(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
) -> impl IntoResponse {
let token_data = match handle_token(token, &app_state, Role::Teacher) {
Err(err) => return err,
Ok(token_data) => token_data,
};
let result = query_as!(
Event,
r#"
SELECT
id,
title,
description,
time_start,
time_end,
event_type AS "event_type!: EventType",
points,
place,
price,
created_by
FROM
events
WHERE
created_by = $1
ORDER BY
time_start
LIMIT
20;
"#,
token_data.id,
)
.fetch_all(&app_state.db_pool)
.await;
match result {
Ok(events) => (StatusCode::OK, Json(json!(events))),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error getting events: {:?}", err)
})),
),
}
.into_response()
}
pub async fn get_recent_events(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
@ -262,62 +215,6 @@ pub async fn get_event(
.into_response()
}
/// Deletes an event from the database.
///
/// If you are a teacher, this only works on your own events.
/// If you are an admin, this works on any event.
pub async fn delete_event(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
Query(get_event_query): Query<GetEventQuery>,
) -> impl IntoResponse {
// validate the JWT
let token_data = match handle_token(token, &app_state, Role::Teacher) {
Err(err) => return err,
Ok(token_data) => token_data,
};
// query the database to delete the record
let result = query!(
r#"
DELETE FROM events
WHERE
id = $1 AND
(created_by = $2 OR $3)
"#,
get_event_query.id,
token_data.id,
token_data.role == Role::Admin,
)
.execute(&app_state.db_pool)
.await;
// handle any possible error from the database
match result {
Ok(result) => {
if result.rows_affected() == 1 {
(StatusCode::OK, Json(json!({})))
} else {
// no reccord could be deleted
(
StatusCode::NOT_FOUND,
Json(json!({
"error": format!("Event {} not found.", get_event_query.id)
})),
)
}
}
// unknown database error
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error getting event: {:?}", err)
})),
),
}
.into_response()
}
pub async fn create_event(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
@ -348,7 +245,7 @@ pub async fn create_event(
match result {
Ok(record) => (StatusCode::OK, Json(json!({ "data": record.id }))).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::BAD_REQUEST,
Json(json!({
"error": format!("Unknown error creating event: {:?}", err)
})),

View File

@ -51,9 +51,8 @@ pub async fn list_points(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
) -> impl IntoResponse {
let token_data = match handle_token(token, &app_state, Role::Student) {
Err(err) => return err,
Ok(token_data) => token_data,
if let Err(err) = handle_token(token, &app_state, Role::Student) {
return err;
};
let result = query_as!(
@ -68,14 +67,10 @@ pub async fn list_points(
ON u.id = ea.user_id AND ea.confirmed = true
LEFT JOIN events e
ON ea.event_id = e.id
WHERE
u.grade = $1 AND
u.role = 'student'
GROUP BY u.id
ORDER BY points DESC
;
"#,
token_data.grade,
)
.fetch_all(&app_state.db_pool)
.await;

View File

@ -7,8 +7,6 @@ mod report;
mod user;
mod winner;
use std::net::SocketAddr;
use axum::{
routing::{delete, get, post, put},
Router,
@ -16,7 +14,7 @@ use axum::{
use jsonwebtoken::{DecodingKey, EncodingKey};
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use tower_http::cors::{self, CorsLayer};
use std::net::SocketAddr;
use user::{signin, signup};
#[derive(Clone)]
@ -47,19 +45,12 @@ async fn main() {
.route("/", get(root))
.route("/user/signup", post(signup))
.route("/user/signin", post(signin))
.route(
"/event",
get(events::get_event)
.post(events::create_event)
.delete(events::delete_event),
)
.route("/event", get(events::get_event).post(events::create_event))
.route("/event/preview", get(events::get_events_preview))
.route("/event/future", get(events::get_all_events))
.route("/event/recent", get(events::get_recent_events))
.route("/event/my", get(events::my_events))
.route("/report", get(report::get_report))
.route("/attending/confirm", put(attending::confirm_attending))
.route("/attending", get(attending::get_attending))
.route("/attending/mark", post(attending::mark_attending))
.route("/attending/unmark", delete(attending::unmark_attending))
.route("/leaderboard/my_points", get(leaderboard::get_points))
@ -68,20 +59,12 @@ async fn main() {
.route("/winners/recent", get(winner::get_recent_winners))
.route("/winners/unclaimed", get(winner::get_unclaimed_winners))
.route("/winners/mark", put(winner::mark_claimed))
.route("/prizes", get(winner::get_prizes).post(winner::new_prize))
.with_state(AppState {
db_pool,
jwt_encode,
jwt_decode,
});
// Create CORS midleware to allow the web build of the UI to work.
let cors = CorsLayer::new()
.allow_methods(cors::Any)
.allow_headers(cors::Any)
.allow_origin(cors::Any);
let app = app.layer(cors);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));

View File

@ -19,35 +19,6 @@ pub struct WinnerEntry {
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct PrizeQuery {
pub prize_id: Option<i32>,
pub grade: i32,
}
#[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct PrizeEntry {
pub id: i32,
pub name: String,
pub points_min: i32,
pub description: String,
pub directions_to_claim: String,
}
#[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct NewPrize {
pub name: String,
pub points_min: i32,
pub description: String,
pub directions_to_claim: String,
}
#[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AttendingEntry {
pub user_id: i32,
pub username: String,
}
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct GetAttendingQuery {
pub event_id: i32,
}
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
@ -183,7 +154,6 @@ pub struct Claims {
pub exp: OffsetDateTime,
pub id: i32,
pub username: String,
pub grade: i32,
pub role: Role,
}

View File

@ -46,16 +46,15 @@ pub async fn get_report(
u.username,
u.grade,
COALESCE(SUM(e.points), 0) AS points,
COALESCE(array_agg(e.title), ARRAY[]::varchar(255)[]) AS "event_titles!",
COALESCE(array_agg(e.description), ARRAY[]::text[]) AS "event_discriptions!",
COALESCE(array_agg(e.points), ARRAY[]::INTEGER[]) AS "event_points!",
COALESCE(array_agg(e.event_type), ARRAY[]::event_type[]) AS "event_types!: Vec<String>"
array_agg(e.title) AS "event_titles!",
array_agg(e.description) AS "event_discriptions!",
array_agg(e.points) AS "event_points!",
array_agg(e.event_type) AS "event_types!: Vec<String>"
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.role = 'student'
GROUP BY u.id
ORDER BY u.grade, u.username, points
"#,
@ -67,9 +66,9 @@ pub async fn get_report(
Ok(records) => records,
Err(err) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::BAD_REQUEST,
Json(json!({
"error": format!("Unknown error getting user data: {:?}", err)
"error": format!("Unknown error creating event: {:?}", err)
})),
)
.into_response()
@ -85,9 +84,9 @@ pub async fn get_report(
Ok(report) => report,
Err(err) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::BAD_REQUEST,
Json(json!({
"error": format!("Unknown error rendering report: {:?}", err)
"error": format!("Unknown error creating event: {:?}", err)
})),
)
.into_response()

View File

@ -64,13 +64,12 @@ pub async fn signin(
let result = sqlx::query!(
r#"
SELECT
id,
username,
role AS "role!: Role",
grade
FROM users
WHERE username = $1 AND password = $2
SELECT
id,
username,
role AS "role!: Role"
FROM users
WHERE username = $1 AND password = $2
"#,
signin.username,
pass_hash.as_bytes(),
@ -83,7 +82,6 @@ pub async fn signin(
let claims = Claims {
exp: OffsetDateTime::now_utc() + JWT_LIFETIME,
id: user.id,
grade: user.grade,
username: user.username,
role: user.role,
};
@ -103,17 +101,21 @@ pub async fn signin(
(StatusCode::OK, Json(json!({ "data": token })))
}
Ok(None) => (
StatusCode::UNAUTHORIZED,
Json(json!({
"error": format!("Incorrect username or password")
})),
),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error signing in: {:?}", err)
})),
),
Ok(None) => {
(
StatusCode::UNAUTHORIZED,
Json(json!({
"error": format!("Incorrect username or password")
})),
)
}
Err(err) => {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error signing in: {:?}", err)
})),
)
}
}
}

View File

@ -9,7 +9,7 @@ use axum_auth::AuthBearer;
use serde_json::json;
use sqlx::{query, query_as};
use crate::models::{NewPrize, PrizeEntry, WinnerEntry, WinnerQuery};
use crate::models::{WinnerEntry, WinnerQuery};
use crate::{
jwt::handle_token,
models::{PrizeQuery, Role},
@ -31,7 +31,6 @@ pub async fn select_winners(
SELECT
u.id,
u.username,
u.grade,
COALESCE(SUM(e.points), 0) AS points,
RANDOM() * COALESCE(SUM(e.points), 0) AS random_value
FROM
@ -51,7 +50,6 @@ pub async fn select_winners(
id, points
FROM weighted_users
WHERE points > p.points_min
AND grade = $3
LIMIT 1
) u
WHERE p.id = $1 OR $2
@ -63,7 +61,6 @@ pub async fn select_winners(
"#,
prize_query.prize_id.unwrap_or(0),
prize_query.prize_id.is_none(),
prize_query.grade,
)
.execute(&app_state.db_pool)
.await;
@ -223,78 +220,3 @@ pub async fn mark_claimed(
}
.into_response()
}
pub async fn get_prizes(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
) -> Response {
if let Err(err) = handle_token(token, &app_state, Role::Student) {
return err;
};
let result = query_as!(
PrizeEntry,
r#"
SELECT
*
FROM prizes
"#,
)
.fetch_all(&app_state.db_pool)
.await;
match result {
Ok(prizes) => (StatusCode::OK, Json(json!(prizes))),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error getting winners: {:?}", err)
})),
),
}
.into_response()
}
pub async fn new_prize(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
Json(new_prize): Json<NewPrize>,
) -> Response {
if let Err(err) = handle_token(token, &app_state, Role::Student) {
return err;
};
let result = query_as!(
PrizeEntry,
r#"
INSERT INTO prizes (name, points_min, description, directions_to_claim)
VALUES ($1, $2, $3, $4)
"#,
new_prize.name,
new_prize.points_min,
new_prize.description,
new_prize.directions_to_claim,
)
.execute(&app_state.db_pool)
.await;
match result {
Ok(result) => {
if result.rows_affected() == 1 {
(StatusCode::OK, Json(json!({})))
} else {
(
StatusCode::BAD_REQUEST,
Json(json!({"error": "Failed to insert prize."})),
)
}
}
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error getting winners: {:?}", err)
})),
),
}
.into_response()
}