From f63f64103c1e46aa1c341af91ae7e4df4f8655c4 Mon Sep 17 00:00:00 2001
From: Guilherme Werner <guilherme.werner@tribufu.com>
Date: Sun, 31 Dec 2023 15:26:24 -0300
Subject: [PATCH] Create initial api wrapper (#2)

* Api v0.1?

* Split crates

* Update api
---
 Cargo.toml                |  21 +-
 examples/api.rs           |  10 +
 examples/client.rs        |  11 --
 examples/token.rs         |  19 +-
 src/actix/Cargo.toml      |  20 ++
 src/actix/lib.rs          |  24 +++
 src/api/Cargo.toml        |  29 +++
 src/api/lib.rs            | 394 ++++++++++++++++++++++++++++++++++++++
 src/client.rs             | 171 -----------------
 src/constants/Cargo.toml  |  17 ++
 src/constants/lib.rs      |   3 +
 src/lib.rs                |  13 +-
 src/token.rs              |  11 --
 src/types/Cargo.toml      |  23 +++
 src/{ => types}/games.rs  |   0
 src/types/lib.rs          |   5 +
 src/{ => types}/oauth2.rs |  32 +++-
 src/types/users.rs        |  41 ++++
 18 files changed, 607 insertions(+), 237 deletions(-)
 create mode 100644 examples/api.rs
 delete mode 100644 examples/client.rs
 create mode 100644 src/actix/Cargo.toml
 create mode 100644 src/actix/lib.rs
 create mode 100644 src/api/Cargo.toml
 create mode 100644 src/api/lib.rs
 delete mode 100644 src/client.rs
 create mode 100644 src/constants/Cargo.toml
 create mode 100644 src/constants/lib.rs
 delete mode 100644 src/token.rs
 create mode 100644 src/types/Cargo.toml
 rename src/{ => types}/games.rs (100%)
 create mode 100644 src/types/lib.rs
 rename src/{ => types}/oauth2.rs (88%)
 create mode 100644 src/types/users.rs

diff --git a/Cargo.toml b/Cargo.toml
index 0006173..06eecfc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@
 name = "tribufu"
 version = "0.0.4"
 description = "Tribufu SDK"
-repository = "https://github.com/Tribufu/SDK-Rust"
+repository = "https://github.com/Tribufu/TribufuRust"
 authors = ["Tribufu <contact@tribufu.com>"]
 license = "Apache-2.0"
 readme = "README.md"
@@ -11,20 +11,23 @@ publish = true
 
 exclude = [".github/", ".vscode/", ".editorconfig", ".gitattributes"]
 
+[workspace]
+resolver = "2"
+members = ["src/*"]
+
 [lib]
 name = "tribufu"
 crate-type = ["rlib"]
 path = "src/lib.rs"
 
+[features]
+actix = ["tribufu-actix"]
+
 [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"
+tribufu-api = { path = "./src/api" }
+tribufu-constants = { path = "./src/constants" }
+tribufu-types = { path = "./src/types" }
+tribufu-actix = { path = "./src/actix", optional = true }
 
 [dev-dependencies]
 dotenv = "0.15.0"
diff --git a/examples/api.rs b/examples/api.rs
new file mode 100644
index 0000000..eb1b2c3
--- /dev/null
+++ b/examples/api.rs
@@ -0,0 +1,10 @@
+// Copyright (c) Tribufu. All Rights Reserved
+
+use tribufu::*;
+
+#[tokio::main]
+async fn main() {
+    let api = TribufuApi::default();
+    let games = api.get_games(Some(1)).await.unwrap();
+    println!("{:?}", games);
+}
diff --git a/examples/client.rs b/examples/client.rs
deleted file mode 100644
index 152db3b..0000000
--- a/examples/client.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-// 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
index b06bd6d..133240d 100644
--- a/examples/token.rs
+++ b/examples/token.rs
@@ -1,23 +1,10 @@
 // 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::<u64>().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);
-    });
+    let api = TribufuApi::with_client_from_env().unwrap_or_default();
+    let games = api.get_games(Some(1)).await.unwrap();
+    println!("{:?}", games);
 }
diff --git a/src/actix/Cargo.toml b/src/actix/Cargo.toml
new file mode 100644
index 0000000..9b7a3db
--- /dev/null
+++ b/src/actix/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "tribufu-actix"
+version = "0.0.4"
+description = "Tribufu Actix Extension"
+repository = "https://github.com/Tribufu/TribufuRust"
+authors = ["Tribufu <contact@tribufu.com>"]
+license = "Apache-2.0"
+readme = "README.md"
+edition = "2021"
+publish = true
+
+[lib]
+name = "tribufu_actix"
+crate-type = ["rlib"]
+path = "lib.rs"
+
+[dependencies]
+actix-web = { version = "4", features = ["rustls"] }
+mintaka-error = { version = "0.0.1" }
+tribufu-api = { path = "../api" }
diff --git a/src/actix/lib.rs b/src/actix/lib.rs
new file mode 100644
index 0000000..a3c5a74
--- /dev/null
+++ b/src/actix/lib.rs
@@ -0,0 +1,24 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+
+use actix_web::HttpRequest;
+use tribufu_api::TribufuApi;
+
+pub trait TribufuApiActixExtension {
+    fn from_actix(req: &HttpRequest) -> Self;
+}
+
+impl TribufuApiActixExtension for TribufuApi {
+    fn from_actix(req: &HttpRequest) -> Self {
+        let mut api = Self::with_client_from_env().unwrap_or_default();
+
+        if let Some(authorization) = req.headers().get("Authorization") {
+            let authorization = authorization.to_str().unwrap();
+
+            if authorization.starts_with("Bearer ") {
+                api = Self::with_user(authorization[7..].to_string());
+            }
+        }
+
+        return api;
+    }
+}
diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml
new file mode 100644
index 0000000..db97e68
--- /dev/null
+++ b/src/api/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "tribufu-api"
+version = "0.0.4"
+description = "Tribufu API"
+repository = "https://github.com/Tribufu/TribufuRust"
+authors = ["Tribufu <contact@tribufu.com>"]
+license = "Apache-2.0"
+readme = "README.md"
+edition = "2021"
+publish = true
+
+[lib]
+name = "tribufu_api"
+crate-type = ["rlib"]
+path = "lib.rs"
+
+[dependencies]
+tribufu-constants = { path = "../constants" }
+tribufu-types = { path = "../types" }
+base64 = "0.21.5"
+mintaka-error = { version = "0.0.1" }
+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"
diff --git a/src/api/lib.rs b/src/api/lib.rs
new file mode 100644
index 0000000..cf6d890
--- /dev/null
+++ b/src/api/lib.rs
@@ -0,0 +1,394 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+
+use alnilam_consts::TARGET_TRIPLE;
+use base64::engine::general_purpose::STANDARD as BASE64;
+use base64::Engine as _;
+use mintaka_error::{Error, Result};
+use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
+use reqwest::Client;
+use std::env;
+use tribufu_constants::VERSION;
+use tribufu_types::games::Game;
+use tribufu_types::oauth2::{OAuth2GrantType, OAuth2TokenRequest, OAuth2TokenResponse};
+use tribufu_types::users::*;
+
+pub enum Credentials {
+    Anonymous,
+    ApiKey {
+        api_key: String,
+    },
+    Client {
+        client_id: u64,
+        client_secret: String,
+    },
+}
+
+pub enum Token {
+    ApiKey {
+        api_key: String,
+    },
+    Basic {
+        basic_token: String,
+    },
+    Bearer {
+        access_token: String,
+        refresh_token: Option<String>,
+    },
+}
+
+pub struct TribufuApi {
+    base_url: String,
+    credentials: Credentials,
+    token: Option<Token>,
+    http: Client,
+}
+
+impl Default for TribufuApi {
+    fn default() -> Self {
+        Self::new(Credentials::Anonymous)
+    }
+}
+
+impl TribufuApi {
+    const TRIBUFU_API_URL: &'static str = "https://api.tribufu.com";
+
+    pub fn new(credentials: Credentials) -> Self {
+        let http = Client::builder()
+            .user_agent(Self::user_agent())
+            .default_headers(Self::default_headers())
+            .build()
+            .unwrap();
+
+        Self {
+            base_url: Self::get_base_url(),
+            credentials,
+            token: None,
+            http,
+        }
+    }
+
+    pub fn debug_enabled(&self) -> bool {
+        return cfg!(debug_assertions);
+    }
+
+    #[inline]
+    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 get_base_url() -> String {
+        if cfg!(debug_assertions) {
+            return env::var("TRIBUFU_API_URL")
+                .unwrap_or_else(|_| Self::TRIBUFU_API_URL.to_string());
+        }
+
+        Self::TRIBUFU_API_URL.to_string()
+    }
+
+    pub fn with_api_key(api_key: String) -> Self {
+        Self::new(Credentials::ApiKey { api_key })
+    }
+
+    pub fn with_client(client_id: u64, client_secret: String) -> Self {
+        Self::new(Credentials::Client {
+            client_id,
+            client_secret,
+        })
+    }
+
+    pub fn with_api_key_from_env() -> Option<Self> {
+        if let Ok(api_key) = env::var("TRIBUFU_API_KEY") {
+            Some(Self::with_api_key(api_key))
+        } else {
+            None
+        }
+    }
+
+    pub fn with_client_from_env() -> Option<Self> {
+        let client_id = env::var("TRIBUFU_CLIENT_ID");
+        let client_secret = env::var("TRIBUFU_CLIENT_SECRET");
+
+        if let (Ok(client_id), Ok(client_secret)) = (client_id, client_secret) {
+            Some(Self::with_client(client_id.parse().unwrap(), client_secret))
+        } else {
+            None
+        }
+    }
+
+    pub fn set_anonymous(&mut self) {
+        self.credentials = Credentials::Anonymous;
+    }
+
+    pub fn set_api_key(&mut self, api_key: String) {
+        self.credentials = Credentials::ApiKey { api_key };
+    }
+
+    pub fn set_clients(&mut self, client_id: u64, client_secret: String) {
+        self.credentials = Credentials::Client {
+            client_id,
+            client_secret,
+        };
+    }
+
+    pub fn set_basic_token(&mut self, basic_token: String) {
+        self.token = Some(Token::Basic { basic_token });
+    }
+
+    pub fn set_bearer_token(&mut self, access_token: String, refresh_token: Option<String>) {
+        self.token = Some(Token::Bearer {
+            access_token,
+            refresh_token,
+        });
+    }
+
+    #[inline]
+    fn headers(&self) -> HeaderMap {
+        let mut headers = Self::default_headers();
+
+        match &self.token {
+            Some(token) => match token {
+                Token::ApiKey { api_key } => {
+                    headers.insert(
+                        AUTHORIZATION,
+                        HeaderValue::from_str(&format!("ApiKey {}", api_key)).unwrap(),
+                    );
+                }
+                Token::Basic { basic_token } => {
+                    headers.insert(
+                        AUTHORIZATION,
+                        HeaderValue::from_str(&format!("Basic {}", basic_token)).unwrap(),
+                    );
+                }
+                Token::Bearer { access_token, .. } => {
+                    headers.insert(
+                        AUTHORIZATION,
+                        HeaderValue::from_str(&format!("Bearer {}", access_token)).unwrap(),
+                    );
+                }
+            },
+            None => {}
+        }
+
+        headers
+    }
+
+    pub async fn get_token_with_code(
+        &mut self,
+        code: String,
+        client_id: u64,
+        client_secret: String,
+    ) -> Result<OAuth2TokenResponse> {
+        self.get_oauth_token(
+            OAuth2GrantType::AuthorizationCode,
+            Some(code),
+            client_id,
+            client_secret,
+            None,
+            None,
+        )
+        .await
+    }
+
+    pub async fn get_token_from_password(
+        &mut self,
+        username: String,
+        password: String,
+        client_id: u64,
+        client_secret: String,
+    ) -> Result<OAuth2TokenResponse> {
+        self.get_oauth_token(
+            OAuth2GrantType::Password,
+            Some(password),
+            client_id,
+            client_secret,
+            None,
+            Some(username),
+        )
+        .await
+    }
+
+    pub async fn get_token_from_passkey(
+        &mut self,
+        username: String,
+        passkey: String,
+        client_id: u64,
+        client_secret: String,
+    ) -> Result<OAuth2TokenResponse> {
+        self.get_oauth_token(
+            OAuth2GrantType::Passkey,
+            Some(passkey),
+            client_id,
+            client_secret,
+            None,
+            Some(username),
+        )
+        .await
+    }
+
+    pub async fn refresh_token(
+        &mut self,
+        refresh_token: String,
+        client_id: u64,
+        client_secret: String,
+    ) -> Result<OAuth2TokenResponse> {
+        self.get_oauth_token(
+            OAuth2GrantType::RefreshToken,
+            Some(refresh_token),
+            client_id,
+            client_secret,
+            None,
+            None,
+        )
+        .await
+    }
+
+    pub async fn get_client_token(
+        &mut self,
+        client_id: u64,
+        client_secret: String,
+    ) -> Result<OAuth2TokenResponse> {
+        self.get_oauth_token(
+            OAuth2GrantType::ClientCredentials,
+            None,
+            client_id,
+            client_secret,
+            None,
+            None,
+        )
+        .await
+    }
+
+    pub async fn get_server_token(
+        &mut self,
+        server_id: u64,
+        client_id: u64,
+        client_secret: String,
+    ) -> Result<OAuth2TokenResponse> {
+        self.get_oauth_token(
+            OAuth2GrantType::ClientCredentials,
+            None,
+            client_id,
+            client_secret,
+            Some("server_id".to_string()),
+            Some(server_id.to_string()),
+        )
+        .await
+    }
+
+    async fn get_oauth_token(
+        &self,
+        grant_type: OAuth2GrantType,
+        grant_value: Option<String>,
+        client_id: u64,
+        client_secret: String,
+        subject_key: Option<String>,
+        subject_value: Option<String>,
+    ) -> Result<OAuth2TokenResponse> {
+        let code = if grant_type == OAuth2GrantType::AuthorizationCode {
+            grant_value.clone()
+        } else {
+            None
+        };
+
+        let refresh_token = if grant_type == OAuth2GrantType::RefreshToken {
+            grant_value.clone()
+        } else {
+            None
+        };
+
+        let mut require_username = false;
+
+        let password = if grant_type == OAuth2GrantType::Password {
+            require_username = true;
+            grant_value.clone()
+        } else {
+            None
+        };
+
+        let passkey = if grant_type == OAuth2GrantType::Passkey {
+            require_username = true;
+            grant_value.clone()
+        } else {
+            None
+        };
+
+        let username = if require_username && subject_value.is_some() {
+            subject_value.clone()
+        } else {
+            None
+        };
+
+        let request_body = OAuth2TokenRequest {
+            grant_type,
+            code,
+            refresh_token,
+            username,
+            password,
+            passkey,
+            client_id: Some(client_id.to_string()),
+            client_secret: Some(client_secret.clone()),
+            redirect_uri: None,
+        };
+
+        let params = if subject_key.is_some() && subject_value.is_some() {
+            format!("?{}={}", subject_key.unwrap(), subject_value.unwrap())
+        } else {
+            "".to_string()
+        };
+
+        let url = format!("{}/v1/oauth2/token{}", self.base_url, params);
+        let headers = self.headers();
+        let response = self
+            .http
+            .post(url)
+            .headers(headers)
+            .form(&request_body)
+            .send()
+            .await?;
+
+        if response.status() != 200 {
+            return Err(Error::msg(format!(
+                "Failed to get token: {}",
+                response.status()
+            )));
+        }
+
+        Ok(response.json().await?)
+    }
+
+    pub async fn get_user_info(&self) -> Result<User> {
+        let url = format!("{}/v1/oauth2/userinfo", self.base_url);
+        let headers = self.headers();
+        let response = self.http.get(url).headers(headers).send().await?;
+
+        Ok(response.json().await?)
+    }
+
+    pub async fn get_games(&self, page: Option<u32>) -> Result<Vec<Game>> {
+        let page = page.unwrap_or(1);
+        let url = format!("{}/v1/packages?page={}", self.base_url, page);
+        let headers = self.headers();
+        let response = self.http.get(url).headers(headers).send().await?;
+
+        Ok(response.json().await?)
+    }
+
+    pub async fn get_game(&self, id: u64) -> Result<Game> {
+        let url = format!("{}/v1/packages/{}", self.base_url, id);
+        let headers = self.headers();
+        let response = self.http.get(url).headers(headers).send().await?;
+
+        Ok(response.json().await?)
+    }
+}
diff --git a/src/client.rs b/src/client.rs
deleted file mode 100644
index 9dba9f2..0000000
--- a/src/client.rs
+++ /dev/null
@@ -1,171 +0,0 @@
-// 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<OAuth2TokenResponse>,
-}
-
-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<String>) -> Result<TribufuClient> {
-        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<Client> {
-        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<u64>) -> 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<Vec<Game>> {
-        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<Game> {
-        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/constants/Cargo.toml b/src/constants/Cargo.toml
new file mode 100644
index 0000000..1eefc86
--- /dev/null
+++ b/src/constants/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "tribufu-constants"
+version = "0.0.4"
+description = "Tribufu Constants"
+repository = "https://github.com/Tribufu/TribufuRust"
+authors = ["Tribufu <contact@tribufu.com>"]
+license = "Apache-2.0"
+readme = "README.md"
+edition = "2021"
+publish = true
+
+[lib]
+name = "tribufu_constants"
+crate-type = ["rlib"]
+path = "lib.rs"
+
+[dependencies]
diff --git a/src/constants/lib.rs b/src/constants/lib.rs
new file mode 100644
index 0000000..e0fcb7e
--- /dev/null
+++ b/src/constants/lib.rs
@@ -0,0 +1,3 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+
+pub const VERSION: &str = env!("CARGO_PKG_VERSION");
diff --git a/src/lib.rs b/src/lib.rs
index 040dee1..d5ea72e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,12 +1,5 @@
 // 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::*;
+pub use tribufu_api::*;
+pub use tribufu_constants::VERSION;
+pub use tribufu_types as types;
diff --git a/src/token.rs b/src/token.rs
deleted file mode 100644
index 208bb0e..0000000
--- a/src/token.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-// 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),
-}
diff --git a/src/types/Cargo.toml b/src/types/Cargo.toml
new file mode 100644
index 0000000..1933c92
--- /dev/null
+++ b/src/types/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "tribufu-types"
+version = "0.0.4"
+description = "Tribufu Types"
+repository = "https://github.com/Tribufu/TribufuRust"
+authors = ["Tribufu <contact@tribufu.com>"]
+license = "Apache-2.0"
+readme = "README.md"
+edition = "2021"
+publish = true
+
+[lib]
+name = "tribufu_types"
+crate-type = ["rlib"]
+path = "lib.rs"
+
+[dependencies]
+chrono = { version = "0.4.22", features = ["serde", "rustc-serialize"] }
+derive_more = "0.99.17"
+mintaka-error = { version = "0.0.1" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = { version = "1.0", features = ["raw_value"] }
+serde_with = "3.4.0"
diff --git a/src/games.rs b/src/types/games.rs
similarity index 100%
rename from src/games.rs
rename to src/types/games.rs
diff --git a/src/types/lib.rs b/src/types/lib.rs
new file mode 100644
index 0000000..b6787df
--- /dev/null
+++ b/src/types/lib.rs
@@ -0,0 +1,5 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+
+pub mod games;
+pub mod oauth2;
+pub mod users;
diff --git a/src/oauth2.rs b/src/types/oauth2.rs
similarity index 88%
rename from src/oauth2.rs
rename to src/types/oauth2.rs
index 07b2067..0c74a83 100644
--- a/src/oauth2.rs
+++ b/src/types/oauth2.rs
@@ -2,38 +2,39 @@
 
 use serde::{Deserialize, Serialize};
 
-#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum OAuth2ResponseType {
     Code,
     Token,
 }
 
-#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum OAuth2ClientType {
     Confidential,
     Public,
 }
 
-#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum OAuth2TokenHintType {
     AccessToken,
     RefreshToken,
 }
 
-#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum OAuth2GrantType {
     AuthorizationCode,
     ClientCredentials,
     DeviceCode,
+    Passkey,
     Password,
     RefreshToken,
 }
 
-#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum OAuth2AuthorizeError {
     AccessDenied,
@@ -45,7 +46,7 @@ pub enum OAuth2AuthorizeError {
     UnsupportedResponseType,
 }
 
-#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum OAuth2TokenType {
     Bearer,
@@ -86,34 +87,47 @@ pub struct OAuth2ErrorResponse {
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct OAuth2TokenRequest {
     pub grant_type: OAuth2GrantType,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub code: Option<String>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub refresh_token: Option<String>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub username: Option<String>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub password: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub passkey: Option<String>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub client_id: Option<String>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub client_secret: Option<String>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub redirect_uri: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub server_id: Option<String>,
 }
 
 #[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<String>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub scope: Option<String>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     pub state: Option<String>,
+
     pub expires_in: u64,
 }
 
@@ -141,7 +155,7 @@ pub struct OAuth2IntrospectionResponse {
 }
 
 impl OAuth2IntrospectionResponse {
-    fn inative() -> Self {
+    pub fn inative() -> Self {
         Self {
             active: false,
             client_id: None,
diff --git a/src/types/users.rs b/src/types/users.rs
new file mode 100644
index 0000000..599bf66
--- /dev/null
+++ b/src/types/users.rs
@@ -0,0 +1,41 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+
+use chrono::{NaiveDate, NaiveDateTime};
+use serde::{Deserialize, Serialize};
+use serde_with::{serde_as, DisplayFromStr};
+
+#[repr(u8)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum UserType {
+    User = 0,
+    Bot = 1,
+    Org = 2,
+}
+
+#[serde_as]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct User {
+    #[serde_as(as = "DisplayFromStr")]
+    pub id: u64,
+    pub uuid: String,
+    pub name: String,
+    pub display_name: String,
+    #[serde(rename = "type")]
+    pub kind: UserType,
+    pub public_flags: u64,
+    pub verified: bool,
+    pub level: u32,
+    pub experience: f64,
+    pub public_birthday: bool,
+    pub birthday: Option<NaiveDate>,
+    pub points: f64,
+    pub location: Option<String>,
+    pub photo_url: Option<String>,
+    pub banner_url: Option<String>,
+    pub last_online: Option<NaiveDateTime>,
+    pub biography: Option<String>,
+    pub view_count: u32,
+    pub created: NaiveDateTime,
+    pub updated: Option<NaiveDateTime>,
+}