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..dea9ace --- /dev/null +++ b/src/api/controllers/lolesports.rs @@ -0,0 +1,291 @@ +use crate::{ + models::{league_with_streams::LeagueWithStreams, stream::Stream}, + 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("/leagues-with-streams")] +async fn leagues_with_streams( + key_result: ApiKeyResult, +) -> Result>>, ApiKeyError> { + match key_result { + ApiKeyResult::Ok(_key) => { + let all_leagues: Result, _> = League::find_all().await; + + let query = format!( + "SELECT + s.league_id, + s.league_id, + st.provider, + st.parameter, + st.locale, + st.english_name + FROM + public.stream st + JOIN + public.schedule s ON st.event_id = s.id AND s.state = 'inProgress'" + ); + + let streams = Stream::query(query, [], "") + .await + .map(|r| r.into_results::()); + + match (all_leagues, streams) { + (Ok(leagues), Ok(current_streams)) => { + let mut leagues_with_streams: Vec = Vec::new(); + + for league in leagues { + let streams_for_league: Vec = current_streams + .iter() + .filter(|stream| stream.league_id == Some(league.id.into())) + .cloned() + .collect(); + let league_with_streams = + LeagueWithStreams::from_league_and_streams(league, streams_for_league); + + leagues_with_streams.push(league_with_streams); + } + Ok(status::Custom(Status::Accepted, Json(leagues_with_streams))) + } + // TODO Implement Error Control + _ => Ok(status::Custom(Status::InternalServerError, Json(vec![]))), + } + } + 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 query_teams = format!( + "SELECT * FROM team t + WHERE t.\"name\" ILIKE '%{query}%' + OR t.slug ILIKE '%{query}%' + OR t.code ILIKE '%{query}%' + ORDER BY t.id DESC" + ); + + let query_players = format!( + "SELECT * FROM player p + WHERE p.first_name ILIKE '%{query}%' + OR p.last_name ILIKE '%{query}%' + OR p.summoner_name ILIKE '%{query}%'" + ); + + let all_teams = Team::query(query_teams, [], "") + .await + .map(|r| r.into_results::()); + + let all_players = Player::query(query_players, [], "") + .await + .map(|r| r.into_results::()); + + 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, + leagues_with_streams, + 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..1f39471 --- /dev/null +++ b/src/api/request_handling/api_key.rs @@ -0,0 +1,65 @@ +use rocket::response::{self}; + +use dotenvy_macro::dotenv; +use rocket::http::Status; +use rocket::request::{FromRequest, Outcome}; +use rocket::response::Responder; +use rocket::{Request, Response}; +use serde_json::json; + +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 f98aba6..893efd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,239 +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::{operators::Comp, ops::QueryBuilder}, -}; - -use models::{ - leagues::League, players::*, search_bar::SearchBarData, stream::Stream, teams::*, - tournaments::Tournament, ts::TeamSchedule, -}; - -use rocket::serde::json::Json; -use utils::triforce_catalog::TriforceCatalog; - -use crate::models::league_with_streams::LeagueWithStreams; - -#[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("/leagues-with-streams")] -async fn leagues_with_streams() -> status::Custom>> { - let all_leagues: Result, _> = League::find_all().await; - - let query = format!( - "SELECT - s.league_id, - s.league_id, - st.provider, - st.parameter, - st.locale, - st.english_name - FROM - public.stream st - JOIN - public.schedule s ON st.event_id = s.id AND s.state = 'inProgress'" - ); - - let streams = Stream::query(query, [], "") - .await - .map(|r| r.into_results::()); - - match (all_leagues, streams) { - (Ok(leagues), Ok(current_streams)) => { - let mut leagues_with_streams: Vec = Vec::new(); - - for league in leagues { - let streams_for_league: Vec = current_streams - .iter() - .filter(|stream| stream.league_id == Some(league.id.into())) - .cloned() - .collect(); - let league_with_streams = - LeagueWithStreams::from_league_and_streams(league, streams_for_league); - - leagues_with_streams.push(league_with_streams); - } - status::Custom(Status::Accepted, Json(leagues_with_streams)) - } - // TODO Implement Error Control - _ => 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(); - - let query_teams = format!( - "SELECT * FROM team t - WHERE t.\"name\" ILIKE '%{query}%' - OR t.slug ILIKE '%{query}%' - OR t.code ILIKE '%{query}%' - ORDER BY t.id DESC" - ); - - let query_players = format!( - "SELECT * FROM player p - WHERE p.first_name ILIKE '%{query}%' - OR p.last_name ILIKE '%{query}%' - OR p.summoner_name ILIKE '%{query}%'" - ); - - let all_teams = Team::query(query_teams, [], "") - .await - .map(|r| r.into_results::()); - - let all_players = Player::query(query_players, [], "") - .await - .map(|r| r.into_results::()); - - 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, - leagues_with_streams, - tournaments, - preview_incoming_events, - find_team_schedule, - teams, - players, - search_bar_data - ], - ) + .mount("/api", lolesports::routes()) .launch() .await - .ok(); // TODO Tremendous error handling instead .ok() + .ok(); }