From 75e88c3ff594a6201223f8d44a8c283c3b4d71c3 Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi Date: Wed, 14 Jun 2023 18:05:24 +0200 Subject: [PATCH] Added x-api-key requirement for requests. Added control of env variables on compile time. Hierarchy changes to split the code --- .gitignore | 3 +- Cargo.toml | 4 +- canyon.toml | 8 +- src/api/controllers/lolesports.rs | 225 ++++++++++++++++++++++++++++ src/api/controllers/mod.rs | 1 + src/api/mod.rs | 2 + src/api/request_handling/api_key.rs | 65 ++++++++ src/api/request_handling/mod.rs | 1 + src/main.rs | 179 +--------------------- src/models/leagues.rs | 4 +- src/models/mod.rs | 8 +- src/models/players.rs | 6 +- src/models/schedules.rs | 6 +- src/models/search_bar.rs | 4 +- src/models/teams.rs | 4 +- src/models/tournaments.rs | 6 +- src/models/ts.rs | 6 +- src/utils/mod.rs | 2 +- src/utils/triforce_catalog.rs | 5 +- 19 files changed, 335 insertions(+), 204 deletions(-) create mode 100644 src/api/controllers/lolesports.rs create mode 100644 src/api/controllers/mod.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/request_handling/api_key.rs create mode 100644 src/api/request_handling/mod.rs diff --git a/.gitignore b/.gitignore index 9bf69e9..99dc5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target /Cargo.lock secrets.toml -vscode/ \ No newline at end of file +vscode/ +.env diff --git a/Cargo.toml b/Cargo.toml index 230bbe1..466b25d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,6 @@ rocket = { version = "0.5.0-rc.2", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # canyon_sql = { git = "https://github.com/zerodaycode/Canyon-SQL.git" } -canyon_sql = { version = "0.4.2", features = ["postgres"] } \ No newline at end of file +canyon_sql = { version = "0.4.2", features = ["postgres"] } +dotenvy = "0.15.7" +dotenvy_macro = "0.15.7" diff --git a/canyon.toml b/canyon.toml index b7ed097..1f36830 100644 --- a/canyon.toml +++ b/canyon.toml @@ -4,9 +4,9 @@ name = 'postgres' [canyon_sql.datasources.auth] -postgresql = { basic = { username = 'zdc', password = 'ggprueba'}} +postgresql = { basic = { username = 'postgres', password = 'postgres'}} [canyon_sql.datasources.properties] -host = '192.168.1.250' -port = 5432 -db_name = 'triforce' +host = 'localhost' +port = 5438 +db_name = 'postgres' diff --git a/src/api/controllers/lolesports.rs b/src/api/controllers/lolesports.rs new file mode 100644 index 0000000..3c3cc97 --- /dev/null +++ b/src/api/controllers/lolesports.rs @@ -0,0 +1,225 @@ +use crate::utils::triforce_catalog::TriforceCatalog; +use rocket::get; +use rocket::http::Status; +use rocket::response::status; +use rocket::serde::json::Json; + +use crate::api::request_handling::api_key::{ApiKeyError, ApiKeyResult}; + +use canyon_sql::{ + crud::{CrudOperations, Transaction}, + query::{operators::Comp, ops::QueryBuilder}, +}; + +use crate::models::{ + leagues::League, players::*, search_bar::SearchBarData, teams::*, tournaments::Tournament, + ts::TeamSchedule, +}; + +#[get("/leagues")] +async fn leagues( + key_result: ApiKeyResult, +) -> Result>>, ApiKeyError> { + match key_result { + ApiKeyResult::Ok(_key) => { + let all_leagues: Result, _> = League::find_all().await; + match all_leagues { + Ok(leagues) => Ok(status::Custom(Status::Accepted, Json(leagues))), + Err(e) => { + eprintln!("Error on leagues: {:?}", e); + Ok(status::Custom(Status::InternalServerError, Json(vec![]))) + } + } + } + ApiKeyResult::Err(err) => Err(err), + } +} + +#[get("/tournaments")] +async fn tournaments( + key_result: ApiKeyResult, +) -> Result>>, ApiKeyError> { + match key_result { + ApiKeyResult::Ok(_key) => { + let all_tournaments: Vec = Tournament::find_all_unchecked().await; + Ok(status::Custom(Status::Accepted, Json(all_tournaments))) + } + ApiKeyResult::Err(err) => Err(err), + } +} + +#[get("/preview-incoming-events")] +async fn preview_incoming_events( + key_result: ApiKeyResult, +) -> Result>>, ApiKeyError> { + match key_result { + ApiKeyResult::Ok(_key) => { + let query = format!( + "SELECT s.id, s.start_time, s.state, s.event_type, s.blockname, s.match_id, s.strategy, s.strategy_count, + s.team_left_id, s.team_left_wins, s.team_right_id, s.team_right_wins, + tl.code AS team_left_name, + tr.code AS team_right_name, + tl.image_url AS team_left_img_url, + tr.image_url AS team_right_img_url, + l.\"name\" AS league_name + FROM schedule s + JOIN team tl ON s.team_left_id = tl.id + JOIN team tr ON s.team_right_id = tr.id + JOIN league l ON s.league_id = l.id + WHERE s.state <> 'completed' + AND s.event_type = 'match' + AND NOT (tl.slug = 'tbd' AND tr.slug = 'tbd') + ORDER BY s.start_time ASC + FETCH FIRST 30 ROWS ONLY" + ); + let schedules = TeamSchedule::query(query, [], "") + .await + .map(|r| r.into_results::()); + match schedules { + Ok(v) => Ok(status::Custom(Status::Accepted, Json(v))), + Err(e) => { + eprintln!("{e}"); + Ok(status::Custom( + Status::InternalServerError, + Json(Vec::new()), + )) + } + } + } + ApiKeyResult::Err(err) => Err(err), + } +} + +#[get("/team//schedule")] +async fn find_team_schedule( + key_result: ApiKeyResult, + team_id: i64, +) -> Result>>, ApiKeyError> { + match key_result { + ApiKeyResult::Ok(_key) => { + let query = format!( + "SELECT s.id, s.start_time, s.state, s.event_type, s.blockname, s.match_id, s.strategy, s.strategy_count, + s.team_left_id, s.team_left_wins, s.team_right_id, s.team_right_wins, + tl.name AS team_left_name, + tr.name AS team_right_name, + tl.image_url AS team_left_img_url, + tr.image_url AS team_right_img_url, + l.\"name\" AS league_name + FROM schedule s + JOIN team tl ON s.team_left_id = tl.id + JOIN team tr ON s.team_right_id = tr.id + JOIN league l ON s.league_id = l.id + WHERE s.team_left_id = {team_id} OR s.team_right_id = {team_id} + ORDER BY s.start_time DESC" + ); + + let schedules = TeamSchedule::query(query, [], "") + .await + .map(|r| r.into_results::()); + + match schedules { + Ok(v) => Ok(status::Custom(Status::Accepted, Json(v))), + Err(e) => { + eprintln!("{e}"); + Ok(status::Custom( + Status::InternalServerError, + Json(Vec::new()), + )) // TODO Replace the empty json + } + } + } + ApiKeyResult::Err(err) => Err(err), + } +} + +#[get("/teams")] +async fn teams(key_result: ApiKeyResult) -> Result>>, ApiKeyError> { + match key_result { + ApiKeyResult::Ok(_key) => { + let all_teams: Vec = Team::find_all_unchecked().await; + Ok(status::Custom(Status::Accepted, Json(all_teams))) + } + ApiKeyResult::Err(err) => Err(err), + } +} + +#[get("/players")] +async fn players( + key_result: ApiKeyResult, +) -> Result>>, ApiKeyError> { + match key_result { + ApiKeyResult::Ok(_key) => { + let all_players: Vec = Player::find_all_unchecked().await; + Ok(status::Custom(Status::Accepted, Json(all_players))) + } + ApiKeyResult::Err(err) => Err(err), + } +} + +#[get("/search-bar-data/")] +async fn search_bar_data( + key_result: ApiKeyResult, + query: &str, +) -> Result>>, ApiKeyError> { + match key_result { + ApiKeyResult::Ok(_key) => { + let mut search_bar_entities: Vec = Vec::new(); + + let all_teams: Result, _> = Team::select_query() + .r#where(TeamFieldValue::name(&query), Comp::Eq) + .or(TeamFieldValue::slug(&query), Comp::Eq) + .query() + .await; + let all_players: Result, _> = Player::select_query() + .r#where(PlayerFieldValue::first_name(&query), Comp::Eq) + .or(PlayerFieldValue::last_name(&query), Comp::Eq) + .or(PlayerFieldValue::summoner_name(&query), Comp::Eq) + .query() + .await; + + if let Ok(teams) = all_teams { + teams.into_iter().for_each(|team| { + search_bar_entities.push(SearchBarData { + id: team.id, + kind: TriforceCatalog::Team, + entity_name: team.name, + entity_image_url: team.image_url, + entity_alt_data: team.slug, + player_role: None, + }) + }); + } else { + eprintln!("Error on teams: {:?}", all_teams.err().unwrap()); + } + + if let Ok(players) = all_players { + players.into_iter().for_each(|player| { + search_bar_entities.push(SearchBarData { + id: player.id, + kind: TriforceCatalog::Player, + entity_name: player.summoner_name, + entity_image_url: player.image_url.unwrap_or_default(), + entity_alt_data: format!("{} {}", player.first_name, player.last_name), + player_role: Some(player.role), + }) + }); + } else { + eprintln!("Error on players: {:?}", all_players.err().unwrap()); + } + + Ok(status::Custom(Status::Accepted, Json(search_bar_entities))) + } + ApiKeyResult::Err(err) => Err(err), + } +} +pub fn routes() -> Vec { + rocket::routes![ + leagues, + tournaments, + preview_incoming_events, + find_team_schedule, + teams, + players, + search_bar_data + ] +} diff --git a/src/api/controllers/mod.rs b/src/api/controllers/mod.rs new file mode 100644 index 0000000..2b77a11 --- /dev/null +++ b/src/api/controllers/mod.rs @@ -0,0 +1 @@ +pub mod lolesports; diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..061feee --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,2 @@ +pub mod controllers; +pub mod request_handling; diff --git a/src/api/request_handling/api_key.rs b/src/api/request_handling/api_key.rs new file mode 100644 index 0000000..2f31b61 --- /dev/null +++ b/src/api/request_handling/api_key.rs @@ -0,0 +1,65 @@ +use rocket::response::{self}; + +use rocket::http::Status; +use rocket::request::{FromRequest, Outcome}; +use rocket::response::Responder; +use rocket::{Request, Response}; +use serde_json::json; +use dotenvy_macro::dotenv; + +use rocket::http::ContentType; + +use std::io::Cursor as SyncCursor; + +pub struct ApiKey(String); +pub struct ApiKeyError { + message: String, + status: Status, +} +pub enum ApiKeyResult { + Ok(ApiKey), + Err(ApiKeyError), +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for ApiKeyResult { + type Error = std::convert::Infallible; + + async fn from_request( + request: &'r Request<'_>, + ) -> rocket::request::Outcome { + let keys: Vec<_> = request.headers().get("x-api-key").collect(); + match keys.len() { + 0 => Outcome::Success(ApiKeyResult::Err(ApiKeyError { + message: "Missing x-api-key".into(), + status: Status::BadRequest, + })), + 1 if keys[0] == option_env!("API_KEY").unwrap_or(dotenv!("API_KEY")) => { + Outcome::Success(ApiKeyResult::Ok(ApiKey(keys[0].to_string()))) + } + 1 => Outcome::Success(ApiKeyResult::Err(ApiKeyError { + message: "Invalid x-api-key".into(), + status: Status::Unauthorized, + })), + _ => Outcome::Success(ApiKeyResult::Err(ApiKeyError { + message: "Multiple x-api-keys".into(), + status: Status::BadRequest, + })), + } + } +} + +impl<'r> Responder<'r, 'static> for ApiKeyError { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + let mut response = Response::new(); + response.set_status(self.status); + response.set_header(ContentType::JSON); + + let body = json!({ "error": self.message }).to_string(); + let cursor = SyncCursor::new(body.into_bytes()); + + response.set_sized_body(None, cursor); + + Ok(response) + } +} diff --git a/src/api/request_handling/mod.rs b/src/api/request_handling/mod.rs new file mode 100644 index 0000000..b357b66 --- /dev/null +++ b/src/api/request_handling/mod.rs @@ -0,0 +1 @@ +pub mod api_key; diff --git a/src/main.rs b/src/main.rs index 3f531fb..893efd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,183 +1,16 @@ extern crate rocket; +mod api; mod models; mod utils; -use rocket::get; -use rocket::http::Status; -use rocket::response::status; - -use canyon_sql::{crud::{CrudOperations, Transaction}, query::{ops::QueryBuilder, operators::Comp}}; - -use models::{ - leagues::League, - tournaments::Tournament, - teams::*, - players::*, - search_bar::SearchBarData, - ts::TeamSchedule -}; - -use rocket::serde::json::Json; -use utils::triforce_catalog::TriforceCatalog; - -#[get("/leagues")] -async fn leagues() -> status::Custom>> { - let all_leagues: Result, _> = League::find_all().await; - match all_leagues { - Ok(leagues) => status::Custom(Status::Accepted, Json(leagues)), - Err(e) => { - eprintln!("Error on leagues: {:?}", e); - status::Custom(Status::InternalServerError, Json(vec![])) - } - } -} - -#[get("/tournaments")] -async fn tournaments() -> status::Custom>> { - let all_tournaments: Vec = Tournament::find_all_unchecked().await; - status::Custom(Status::Accepted, Json(all_tournaments)) -} - -#[get("/preview-incoming-events")] -async fn preview_incoming_events() -> status::Custom>> { - let query = format!( - "SELECT s.id, s.start_time, s.state, s.event_type, s.blockname, s.match_id, s.strategy, s.strategy_count, - s.team_left_id, s.team_left_wins, s.team_right_id, s.team_right_wins, - tl.code AS team_left_name, - tr.code AS team_right_name, - tl.image_url AS team_left_img_url, - tr.image_url AS team_right_img_url, - l.\"name\" AS league_name - FROM schedule s - JOIN team tl ON s.team_left_id = tl.id - JOIN team tr ON s.team_right_id = tr.id - JOIN league l ON s.league_id = l.id - WHERE s.state <> 'completed' - AND s.event_type = 'match' - AND NOT (tl.slug = 'tbd' AND tr.slug = 'tbd') - ORDER BY s.start_time ASC - FETCH FIRST 30 ROWS ONLY" - ); - - let schedules = TeamSchedule::query(query, [], "") - .await - .map(|r| r.into_results::()); - - match schedules { - Ok(v) => status::Custom(Status::Accepted, Json(v)), - Err(e) => { - eprintln!("{e}"); - status::Custom(Status::InternalServerError, Json(Vec::new())) // TODO Replace the empty json - }, - } -} - -#[get("/team//schedule")] -async fn find_team_schedule(team_id: i64) -> status::Custom>> { - let query = format!( - "SELECT s.id, s.start_time, s.state, s.event_type, s.blockname, s.match_id, s.strategy, s.strategy_count, - s.team_left_id, s.team_left_wins, s.team_right_id, s.team_right_wins, - tl.name AS team_left_name, - tr.name AS team_right_name, - tl.image_url AS team_left_img_url, - tr.image_url AS team_right_img_url, - l.\"name\" AS league_name - FROM schedule s - JOIN team tl ON s.team_left_id = tl.id - JOIN team tr ON s.team_right_id = tr.id - JOIN league l ON s.league_id = l.id - WHERE s.team_left_id = {team_id} OR s.team_right_id = {team_id} - ORDER BY s.start_time DESC" - ); - - let schedules = TeamSchedule::query(query, [], "") - .await - .map(|r| r.into_results::()); - - match schedules { - Ok(v) => status::Custom(Status::Accepted, Json(v)), - Err(e) => { - eprintln!("{e}"); - status::Custom(Status::InternalServerError, Json(Vec::new())) // TODO Replace the empty json - }, - } -} - -#[get("/teams")] -async fn teams() -> status::Custom>> { - let all_teams: Vec = Team::find_all_unchecked().await; - status::Custom(Status::Accepted, Json(all_teams)) -} - - -#[get("/players")] -async fn players() -> status::Custom>> { - let all_players: Vec = Player::find_all_unchecked().await; - status::Custom(Status::Accepted, Json(all_players)) -} - -#[get("/search-bar-data/")] -async fn search_bar_data(query: &str) -> status::Custom>> { - let mut search_bar_entities: Vec = Vec::new(); - - // TODO Replace for .like(...) clauses when released - let all_teams: Result, _> = Team::select_query() - .r#where(TeamFieldValue::name(&query), Comp::Eq) - .or(TeamFieldValue::slug(&query), Comp::Eq) - .query() - .await; - let all_players: Result, _> = Player::select_query() - .r#where(PlayerFieldValue::first_name(&query), Comp::Eq) - .or(PlayerFieldValue::last_name(&query), Comp::Eq) - .or(PlayerFieldValue::summoner_name(&query), Comp::Eq) - .query() - .await; - - if let Ok(teams) = all_teams { - teams.into_iter().for_each(|team| - search_bar_entities.push(SearchBarData { - id: team.id, - kind: TriforceCatalog::Team, - entity_name: team.name, - entity_image_url: team.image_url, - entity_alt_data: team.slug, - player_role: None, - }) - ); - } // TODO Else clause matching an Err kind - - if let Ok(players) = all_players { - players.into_iter().for_each(|player| - search_bar_entities.push(SearchBarData { - id: player.id, - kind: TriforceCatalog::Player, - entity_name: player.summoner_name, - entity_image_url: player.image_url.unwrap_or_default(), - entity_alt_data: format!("{} {}", player.first_name, player.last_name), - player_role: Some(player.role), - }) - ); - } // TODO Else clause matching an Err kind - - status::Custom(Status::Accepted, Json(search_bar_entities)) -} +use api::controllers::lolesports; #[canyon_sql::main] fn main() { rocket::build() - .mount( - "/api", - rocket::routes![ - leagues, - tournaments, - preview_incoming_events, - find_team_schedule, - teams, - players, - search_bar_data - ] - ).launch() + .mount("/api", lolesports::routes()) + .launch() .await - .ok(); // TODO Tremendous error handling instead .ok() -} \ No newline at end of file + .ok(); +} diff --git a/src/models/leagues.rs b/src/models/leagues.rs index 0ae97bb..2eaa551 100644 --- a/src/models/leagues.rs +++ b/src/models/leagues.rs @@ -1,5 +1,5 @@ -use serde::Serialize; use canyon_sql::macros::*; +use serde::Serialize; #[derive(Debug, Clone, CanyonCrud, CanyonMapper, Serialize)] #[canyon_entity] @@ -11,4 +11,4 @@ pub struct League { name: String, region: String, image_url: String, -} \ No newline at end of file +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 321228e..a45355d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,7 +1,7 @@ pub mod leagues; -pub mod tournaments; -pub mod teams; pub mod players; -pub mod search_bar; pub mod schedules; -pub mod ts; \ No newline at end of file +pub mod search_bar; +pub mod teams; +pub mod tournaments; +pub mod ts; diff --git a/src/models/players.rs b/src/models/players.rs index acd54f0..2bc1b35 100644 --- a/src/models/players.rs +++ b/src/models/players.rs @@ -1,5 +1,5 @@ -use serde::Serialize; use canyon_sql::macros::*; +use serde::Serialize; #[derive(Debug, Clone, Fields, CanyonCrud, CanyonMapper, Serialize)] #[canyon_entity] @@ -11,5 +11,5 @@ pub struct Player { last_name: String, summoner_name: String, image_url: Option, - role: String -} \ No newline at end of file + role: String, +} diff --git a/src/models/schedules.rs b/src/models/schedules.rs index ac69dc8..21fb96b 100644 --- a/src/models/schedules.rs +++ b/src/models/schedules.rs @@ -1,5 +1,5 @@ +use canyon_sql::{date_time::NaiveDateTime, macros::*}; use serde::Serialize; -use canyon_sql::{macros::*, date_time::NaiveDateTime}; #[derive(Debug, Clone, CanyonCrud, CanyonMapper, Serialize, Fields)] #[canyon_entity] @@ -17,5 +17,5 @@ pub struct Schedule { team_left_id: Option, team_left_wins: Option, team_right_id: Option, - team_right_wins: Option -} \ No newline at end of file + team_right_wins: Option, +} diff --git a/src/models/search_bar.rs b/src/models/search_bar.rs index ca53686..911bbf5 100644 --- a/src/models/search_bar.rs +++ b/src/models/search_bar.rs @@ -1,5 +1,5 @@ -use serde::Serialize; use crate::utils::triforce_catalog::TriforceCatalog; +use serde::Serialize; #[derive(Default, Serialize)] pub struct SearchBarData { @@ -9,4 +9,4 @@ pub struct SearchBarData { pub entity_image_url: String, pub entity_alt_data: String, pub player_role: Option, -} \ No newline at end of file +} diff --git a/src/models/teams.rs b/src/models/teams.rs index 6a63b5c..41b4ed4 100644 --- a/src/models/teams.rs +++ b/src/models/teams.rs @@ -1,5 +1,5 @@ -use serde::Serialize; use canyon_sql::macros::*; +use serde::Serialize; #[derive(Debug, Clone, Fields, CanyonCrud, CanyonMapper, Serialize)] #[canyon_entity] @@ -14,4 +14,4 @@ pub struct Team { alt_image_url: Option, bg_image_url: Option, // home_league: i32 -} \ No newline at end of file +} diff --git a/src/models/tournaments.rs b/src/models/tournaments.rs index bc6f58c..d00ccf6 100644 --- a/src/models/tournaments.rs +++ b/src/models/tournaments.rs @@ -1,5 +1,5 @@ +use canyon_sql::{date_time::NaiveDate, macros::*}; use serde::Serialize; -use canyon_sql::{macros::*, date_time::NaiveDate}; use super::leagues::League; @@ -13,5 +13,5 @@ pub struct Tournament { start_date: NaiveDate, end_date: NaiveDate, #[foreign_key(table = "league", column = "id")] - league: i32 -} \ No newline at end of file + league: i32, +} diff --git a/src/models/ts.rs b/src/models/ts.rs index 145b509..1369dc0 100644 --- a/src/models/ts.rs +++ b/src/models/ts.rs @@ -1,5 +1,5 @@ +use canyon_sql::{date_time::NaiveDateTime, macros::*}; use serde::Serialize; -use canyon_sql::{macros::*, date_time::NaiveDateTime}; #[derive(Debug, Clone, CanyonCrud, CanyonMapper, Serialize, Fields)] #[canyon_entity] @@ -20,6 +20,6 @@ pub struct TeamSchedule { team_right_wins: Option, team_left_img_url: Option, team_left_name: Option, - team_right_img_url: Option, + team_right_img_url: Option, team_right_name: Option, -} \ No newline at end of file +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index b10c453..8fbf978 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1 @@ -pub mod triforce_catalog; \ No newline at end of file +pub mod triforce_catalog; diff --git a/src/utils/triforce_catalog.rs b/src/utils/triforce_catalog.rs index 063e9ce..f9e386c 100644 --- a/src/utils/triforce_catalog.rs +++ b/src/utils/triforce_catalog.rs @@ -4,8 +4,9 @@ use serde::Serialize; #[derive(Debug, Default, Serialize)] #[allow(unused)] pub enum TriforceCatalog { - #[default] League, + #[default] + League, Tournament, Team, - Player + Player, }