From 21daa668f846b82bbfbcec8580c066208921997b Mon Sep 17 00:00:00 2001 From: Guilherme Werner Date: Sun, 24 Dec 2023 08:23:10 -0300 Subject: [PATCH] Prototype API (#1) * Add oauth2 client * Update LICENSE.txt * Create .env.example * Add dotenv to example * Add oauth2 types * Update lib.rs * Update Cargo.toml * Update lib.rs * Add games routes --- .cargo/config.toml | 2 - .editorconfig | 3 + .env.example | 3 + .gitignore | 38 +++++++++- Cargo.toml | 31 ++++---- LICENSE.txt | 2 +- Source/lib.rs | 5 -- examples/client.rs | 11 +++ examples/token.rs | 23 ++++++ src/client.rs | 171 +++++++++++++++++++++++++++++++++++++++++++++ src/games.rs | 29 ++++++++ src/lib.rs | 12 ++++ src/oauth2.rs | 153 ++++++++++++++++++++++++++++++++++++++++ src/token.rs | 11 +++ 14 files changed, 472 insertions(+), 22 deletions(-) delete mode 100644 .cargo/config.toml create mode 100644 .env.example delete mode 100644 Source/lib.rs create mode 100644 examples/client.rs create mode 100644 examples/token.rs create mode 100644 src/client.rs create mode 100644 src/games.rs create mode 100644 src/lib.rs create mode 100644 src/oauth2.rs create mode 100644 src/token.rs diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 420e44e..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target-dir = "Intermediate/Target" diff --git a/.editorconfig b/.editorconfig index 88df879..b903496 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +[*.env*] +insert_final_newline = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..132e464 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +CLIENT_ID= +CLIENT_SECRET= +TRIBUFU_API_URL=https://api.tribufu.com \ No newline at end of file diff --git a/.gitignore b/.gitignore index 20af12c..d5805d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,40 @@ -[Bb]inaries/ -[Ii]ntermediate/ +__pycache__/ +.gradle/ +.idea/ +.metals/ +.next/ +.parcel-cache/ +.vs/ +.vscode/ +bin/ +binaries/ +build/ +node_modules/ +obj/ +saved/ +target/ .DS_Store +.env +*.crt +*.csproj +*.filters +*.fsproj +*.key +*.log +*.make +*.mwb.bak +*.pem +*.sln +*.user +*.vcxproj +*.vpp.* +*.wasm +*.xcodeproj +*.xcworkspace Cargo.lock desktop.ini +keystore.jks +local.properties +Makefile +next-env.d.ts diff --git a/Cargo.toml b/Cargo.toml index 16fa2fe..0006173 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,31 @@ [package] name = "tribufu" -version = "0.0.3" -description = "TribuFu SDK" -repository = "https://github.com/TribuFu/SDK-RS" -authors = ["TribuFu "] +version = "0.0.4" +description = "Tribufu SDK" +repository = "https://github.com/Tribufu/SDK-Rust" +authors = ["Tribufu "] license = "Apache-2.0" readme = "README.md" edition = "2021" publish = true -exclude = [ - ".github/", - ".vscode/", - ".editorconfig", - ".gitattributes", -] +exclude = [".github/", ".vscode/", ".editorconfig", ".gitattributes"] [lib] -name = "TribuFu" +name = "tribufu" crate-type = ["rlib"] -path = "Source/lib.rs" +path = "src/lib.rs" [dependencies] +alnilam-consts = { version = "0.0.4" } +anyhow = "1.0.75" +chrono = { version = "0.4.22", features = ["serde", "rustc-serialize"] } +derive_more = "0.99.17" +reqwest = { version = "0.11.18", features = ["json", "stream"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +serde_with = "3.4.0" + +[dev-dependencies] +dotenv = "0.15.0" +tokio = { version = "1", features = ["full"] } diff --git a/LICENSE.txt b/LICENSE.txt index a9afc15..12ea8be 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) TribuFu. All Rights Reserved + Copyright (c) Tribufu. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/lib.rs b/Source/lib.rs deleted file mode 100644 index 76eeb18..0000000 --- a/Source/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) TribuFu. All Rights Reserved - -#![allow(non_snake_case)] - -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/examples/client.rs b/examples/client.rs new file mode 100644 index 0000000..152db3b --- /dev/null +++ b/examples/client.rs @@ -0,0 +1,11 @@ +// Copyright (c) Tribufu. All Rights Reserved + +use tribufu::*; + +#[tokio::main] +async fn main() { + match TribufuClient::new(0, "client_secret") { + Ok(client) => println!("client_id: {}", client.id()), + Err(e) => println!("error: {:?}", e), + } +} diff --git a/examples/token.rs b/examples/token.rs new file mode 100644 index 0000000..b06bd6d --- /dev/null +++ b/examples/token.rs @@ -0,0 +1,23 @@ +// Copyright (c) Tribufu. All Rights Reserved + +use dotenv::dotenv; +use std::env; +use tribufu::*; + +#[tokio::main] +async fn main() { + dotenv().ok(); + + let client_id = env::var("CLIENT_ID").unwrap().parse::().unwrap(); + let client_secret = env::var("CLIENT_SECRET").unwrap(); + + let mut client = TribufuClient::new(client_id, client_secret).unwrap(); + + client.get_token(None).await.unwrap(); + + let games = client.get_games().await.unwrap(); + + games.iter().for_each(|game| { + println!("{}", game.name); + }); +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..9dba9f2 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,171 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +use crate::games::Game; +use crate::oauth2::*; +use crate::VERSION; +use alnilam_consts::TARGET_TRIPLE; +use anyhow::{Error, Result}; +use reqwest::header; +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::Client; + +#[derive(Clone)] +pub struct TribufuClient { + client_id: u64, + client_secret: String, + token: Option, +} + +impl TribufuClient { + //const BASE_URL: &'static str = "https://api.tribufu.com"; + const BASE_URL: &'static str = "http://localhost:5000"; + + pub fn new(id: u64, secret: impl Into) -> Result { + Ok(TribufuClient { + client_id: id, + client_secret: secret.into(), + token: None, + }) + } + + #[inline] + pub fn id(&self) -> u64 { + self.client_id + } + + #[inline] + pub fn user_agent() -> String { + format!( + "Tribufu/{} (+https://api.tribufu.com; {})", + VERSION, TARGET_TRIPLE + ) + } + + #[inline] + fn default_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("X-Tribufu-Language", HeaderValue::from_static("rust")); + headers.insert("X-Tribufu-Version", HeaderValue::from_static(VERSION)); + headers + } + + fn http_client(&self) -> Result { + let user_agent = Self::user_agent(); + let mut headers = Self::default_headers(); + + if let Some(token) = &self.token { + headers.insert( + header::AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", token.access_token))?, + ); + } + + let http = Client::builder() + .user_agent(user_agent) + .default_headers(headers) + .build()?; + + Ok(http) + } + + pub async fn get_token(&mut self, server_id: Option) -> Result<()> { + let server_id = if let Some(server_id) = server_id { + Some(server_id.to_string()) + } else { + None + }; + + let body = OAuth2TokenRequest { + grant_type: OAuth2GrantType::ClientCredentials, + code: None, + refresh_token: None, + username: None, + password: None, + client_id: Some(self.client_id.to_string()), + client_secret: Some(self.client_secret.clone()), + redirect_uri: None, + server_id, + }; + + let url = format!("{}/v1/oauth2/token", Self::BASE_URL); + let response = self.http_client()?.post(url).form(&body).send().await?; + + if response.status() != 200 { + return Err(Error::msg(format!( + "Failed to get token: {}", + response.status() + ))); + } + + self.token = Some(response.json().await?); + + Ok(()) + } + + pub async fn refresh_token_token(&mut self) -> Result<()> { + let token = if let Some(token) = &self.token { + token + } else { + return Err(Error::msg( + format!("Failed to refresh: self.token == None",), + )); + }; + + if token.refresh_token.is_none() { + return Err(Error::msg(format!( + "Failed to refresh: self.token.refresh_token == None", + ))); + } + + let body = OAuth2TokenRequest { + grant_type: OAuth2GrantType::RefreshToken, + code: None, + refresh_token: token.refresh_token.clone(), + username: None, + password: None, + client_id: Some(self.client_id.to_string()), + client_secret: Some(self.client_secret.clone()), + redirect_uri: None, + server_id: None, + }; + + let url = format!("{}/v1/oauth2/token", Self::BASE_URL); + let response = self.http_client()?.post(url).form(&body).send().await?; + + if response.status() != 200 { + return Err(Error::msg(format!( + "Failed to get token: {}", + response.status() + ))); + } + + self.token = Some(response.json().await?); + + Ok(()) + } + + pub async fn get_games(&self) -> Result> { + let url = format!("{}/v1/packages", Self::BASE_URL); + let response = self.http_client()?.get(url).send().await?; + + Ok(response.json().await?) + } + + pub async fn get_game(&self, id: u64) -> Result { + let url = format!("{}/v1/packages/{}", Self::BASE_URL, id); + let response = self.http_client()?.get(url).send().await?; + + Ok(response.json().await?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_client() { + let client = TribufuClient::new(0, "client_secret").unwrap(); + assert_eq!(client.id(), 0); + } +} diff --git a/src/games.rs b/src/games.rs new file mode 100644 index 0000000..e34c3ad --- /dev/null +++ b/src/games.rs @@ -0,0 +1,29 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; + +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Game { + #[serde_as(as = "DisplayFromStr")] + pub id: u64, + pub name: String, + pub description: Option, + pub icon_url: Option, + pub banner_url: Option, + pub capsule_image_url: Option, + pub library_image_url: Option, + pub slug: Option, + pub game_port: Option, + pub query_port: Option, + pub rcon_port: Option, + pub steam_app_id: Option, + pub steam_server_app_id: Option, + pub rust_gamedig_id: Option, + pub node_gamedig_id: Option, + pub server_connect_url: Option, + pub created: NaiveDateTime, + pub updated: Option, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..040dee1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +#![allow(dead_code)] + +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub mod client; +pub mod games; +pub mod oauth2; +pub mod token; + +pub use client::*; diff --git a/src/oauth2.rs b/src/oauth2.rs new file mode 100644 index 0000000..07b2067 --- /dev/null +++ b/src/oauth2.rs @@ -0,0 +1,153 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OAuth2ResponseType { + Code, + Token, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OAuth2ClientType { + Confidential, + Public, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OAuth2TokenHintType { + AccessToken, + RefreshToken, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OAuth2GrantType { + AuthorizationCode, + ClientCredentials, + DeviceCode, + Password, + RefreshToken, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OAuth2AuthorizeError { + AccessDenied, + InvalidRequest, + InvalidScope, + ServerError, + TemporarilyUnavailable, + UnauthorizedClient, + UnsupportedResponseType, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OAuth2TokenType { + Bearer, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuth2AuthorizeRequest { + pub response_type: OAuth2ResponseType, + pub client_id: String, + pub client_secret: Option, + pub redirect_uri: String, + pub scope: Option, + pub state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuth2CodeResponse { + pub code: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuth2ErrorResponse { + pub error: OAuth2AuthorizeError, + + #[serde(skip_serializing_if = "Option::is_none")] + pub error_description: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub error_uri: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuth2TokenRequest { + pub grant_type: OAuth2GrantType, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_secret: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub redirect_uri: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub server_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuth2TokenResponse { + pub token_type: OAuth2TokenType, + pub access_token: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + pub expires_in: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuth2RevokeRequest { + pub token: String, + pub token_type_hint: OAuth2TokenHintType, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuth2IntrospectionResponse { + pub active: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub client_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub exp: Option, +} + +impl OAuth2IntrospectionResponse { + fn inative() -> Self { + Self { + active: false, + client_id: None, + username: None, + scope: None, + exp: None, + } + } +} diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..208bb0e --- /dev/null +++ b/src/token.rs @@ -0,0 +1,11 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +use crate::oauth2::OAuth2TokenResponse; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Credentials { + ApiKey(String), + Token(OAuth2TokenResponse), +}