diff --git a/Cargo.lock b/Cargo.lock index 20d59a1..5953743 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1535,6 +1535,7 @@ checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" name = "libiam" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "futures", "iam-common", diff --git a/libiam/Cargo.toml b/libiam/Cargo.toml index 1536e21..ac4267a 100644 --- a/libiam/Cargo.toml +++ b/libiam/Cargo.toml @@ -17,3 +17,4 @@ tokio = { version = "1.25.0", features = ["rt"] } futures = "0.3.26" async-trait = "0.1.64" jsonwebtoken = "9.1.0" +anyhow = "1.0.86" diff --git a/libiam/src/api/app.rs b/libiam/src/api/app.rs new file mode 100644 index 0000000..7a84b12 --- /dev/null +++ b/libiam/src/api/app.rs @@ -0,0 +1,22 @@ +use super::Api; +use anyhow::Result; +use reqwest::Method; +use serde::{Deserialize, Serialize}; + +pub mod login { + use super::*; + + #[derive(Debug, Serialize)] + pub struct Request<'a> { + pub token: &'a str, + } + + #[derive(Debug, Deserialize)] + pub struct Response { + pub token: String, + } +} + +pub async fn login(api: &Api, req: &login::Request<'_>) -> Result { + api.request(Method::POST, "/v1/apps/login", Some(req)).await +} diff --git a/libiam/src/api/mod.rs b/libiam/src/api/mod.rs new file mode 100644 index 0000000..beaee0d --- /dev/null +++ b/libiam/src/api/mod.rs @@ -0,0 +1,86 @@ +pub mod app; +pub mod user; + +use reqwest::{header::AUTHORIZATION, Client, Method, Url}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use thiserror::Error; +use tokio::runtime::Handle; + +#[derive(Debug)] +pub struct Api { + client: Client, + base: Url, + handle: Handle, + token: Option, +} + +impl Api { + pub fn new(base: &str, token: Option) -> anyhow::Result { + Ok(Self { + client: Client::new(), + base: Url::parse(base)?, + handle: Handle::current(), + token, + }) + } + + pub fn with_token(&self, token: String) -> Self { + Self { + client: self.client.clone(), + base: self.base.clone(), + handle: self.handle.clone(), + token: Some(token), + } + } + + #[inline] + async fn request( + &self, + method: Method, + path: &str, + req: Option<&Req>, + ) -> anyhow::Result + where + Req: Serialize, + Res: DeserializeOwned + Send + 'static, + { + let mut r = self.client.request(method, self.base.join(path).unwrap()); + + if let Some(token) = &self.token { + r = r.header(AUTHORIZATION, &format!("Bearer {}", token)); + } + + if let Some(req) = req { + r = r.json(req) + } + + self.handle + .spawn(async move { + let res: ErrorOr = r.send().await?.json().await?; + + match res { + ErrorOr::Error(error) => { + tracing::error!("iam error: {error:?}"); + Err(error.into()) + } + ErrorOr::Data(res) => anyhow::Ok(res), + } + }) + .await + .unwrap() + } +} + +#[derive(Debug, Deserialize, Error)] +#[error("{error}")] +pub struct Error { + pub code: String, + pub error: String, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum ErrorOr { + Error(Error), + Data(T), +} diff --git a/libiam/src/api/user.rs b/libiam/src/api/user.rs new file mode 100644 index 0000000..4859c63 --- /dev/null +++ b/libiam/src/api/user.rs @@ -0,0 +1,56 @@ +use super::Api; +use anyhow::Result; +use reqwest::Method; +use serde::{Deserialize, Serialize}; + +pub mod register { + use super::*; + + #[derive(Debug, Serialize)] + pub struct Request<'a> { + pub name: &'a str, + pub email: &'a str, + pub password: &'a str, + } + + #[derive(Debug, Deserialize)] + pub struct Response { + pub id: String, + } +} + +pub async fn register(api: &Api, req: ®ister::Request<'_>) -> Result { + api.request(Method::POST, "/v1/users/register", Some(req)) + .await +} + +pub mod login { + use super::*; + + #[derive(Debug, Serialize)] + pub struct Request<'a> { + pub email: &'a str, + pub password: &'a str, + } + + #[derive(Debug, Deserialize)] + pub struct Response { + pub token: String, + } +} + +pub async fn login(api: &Api, req: &login::Request<'_>) -> Result { + api.request(Method::POST, "/v1/users/login", Some(req)) + .await +} + +pub mod get_user { + use iam_common::user::UserInfo; + + pub type Response = UserInfo; +} + +pub async fn get_user(api: &Api, id: &str) -> Result { + api.request(Method::GET, &format!("/v1/users/{id}"), None::<&()>) + .await +} diff --git a/libiam/src/app.rs b/libiam/src/app.rs index c071fb3..10decdd 100644 --- a/libiam/src/app.rs +++ b/libiam/src/app.rs @@ -1,22 +1,15 @@ -use std::sync::Arc; - -use iam_common::user::UserInfo; -use reqwest::Client; -use serde::Deserialize; -use serde_json::json; - use crate::{ - error::{unwrap_res, ErrorMessage, Result}, - utils::{create_client, Either}, + api::{self, Api}, Iam, }; +use iam_common::user::UserInfo; +use std::sync::Arc; #[derive(Debug)] pub struct AppInner { secret: String, token: String, - iam: Iam, - client: Client, + api: Api, } #[derive(Debug, Clone)] @@ -25,41 +18,20 @@ pub struct App { } impl App { - pub async fn login(iam: &Iam, secret: &str) -> Result { - #[derive(Debug, Deserialize)] - struct Response { - token: String, - } - - tracing::debug!(secret, "app logging into iam"); - - let res = Client::new() - .post(iam.get_url("/v1/apps/login")) - .json(&json!({ - "token": secret, - })) - .send() + pub async fn login(iam: &Iam, secret: &str) -> anyhow::Result { + let token = api::app::login(&iam.inner.api, &api::app::login::Request { token: secret }) .await? - .json::>() - .await?; - - let res = unwrap_res(res)?; + .token; Ok(Self { inner: Arc::new(AppInner { secret: secret.to_owned(), - client: create_client(&res.token), - token: res.token, - iam: iam.clone(), + token: token.clone(), + api: iam.inner.api.with_token(token), }), }) } - #[inline] - fn client(&self) -> &Client { - &self.inner.client - } - pub fn token(&self) -> &str { &self.inner.token } @@ -69,17 +41,8 @@ impl App { id } - pub async fn get_user_info(&self, id: &str) -> Result { - let res = self - .client() - .get(self.inner.iam.get_url(&format!("/v1/users/{}/", id))) - .send() - .await? - .json::>() - .await?; - - let res = unwrap_res(res)?; - + pub async fn get_user_info(&self, id: &str) -> anyhow::Result { + let res = api::user::get_user(&self.inner.api, id).await?; Ok(res) } } diff --git a/libiam/src/error.rs b/libiam/src/error.rs deleted file mode 100644 index 7bbe6f0..0000000 --- a/libiam/src/error.rs +++ /dev/null @@ -1,30 +0,0 @@ -use serde::Deserialize; - -use crate::utils::Either; - -#[derive(Debug, Deserialize)] -pub struct ErrorMessage { - pub code: String, - pub error: String, -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("error returned from iam: {0:?}")] - Iam(ErrorMessage), - #[error("reqwest error: {0}")] - Reqwest(#[from] reqwest::Error), -} - -pub type Result = std::result::Result; - -#[inline] -pub fn unwrap_res(either: Either) -> Result { - match either { - Either::Left(t) => Ok(t), - Either::Right(err) => { - tracing::error!("iam returned error: {err:?}"); - Err(Error::Iam(err)) - } - } -} diff --git a/libiam/src/lib.rs b/libiam/src/lib.rs index 67a8149..2826e18 100644 --- a/libiam/src/lib.rs +++ b/libiam/src/lib.rs @@ -1,9 +1,9 @@ +pub mod api; mod app; -mod error; pub mod testing; mod user; -mod utils; +use api::Api; use std::sync::Arc; pub use app::App; @@ -12,7 +12,7 @@ pub use user::User; #[derive(Debug)] pub struct IamInner { - base_url: String, + api: Api, } #[derive(Debug, Clone)] @@ -24,12 +24,12 @@ impl Iam { pub fn new(base_url: &str) -> Self { Self { inner: Arc::new(IamInner { - base_url: base_url.to_owned(), + api: Api::new(base_url, None).unwrap(), }), } } - pub(crate) fn get_url(&self, path: &str) -> String { - format!("{}{}", self.inner.base_url, path) + pub fn api(&self) -> &Api { + &self.inner.api } } diff --git a/libiam/src/user.rs b/libiam/src/user.rs index 5e185c2..7b157be 100644 --- a/libiam/src/user.rs +++ b/libiam/src/user.rs @@ -1,21 +1,17 @@ use crate::{ - error::{unwrap_res, ErrorMessage, Result}, - utils::Either, + api::{self, Api}, Iam, }; use iam_common::token::Claims; use iam_common::Id; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; -use reqwest::Client; -use serde::Deserialize; -use serde_json::json; use std::sync::Arc; #[derive(Debug)] pub struct UserInner { token: String, id: Id, - _iam: Iam, + _api: Api, } #[derive(Debug, Clone)] @@ -24,50 +20,44 @@ pub struct User { } impl User { - pub async fn register(iam: &Iam, name: &str, email: &str, password: &str) -> Result { - Client::new() - .post(iam.get_url("/v1/users/register")) - .json(&json!({ - "name": name, - "email": email, - "password": password, - })) - .send() - .await?; + pub async fn register( + iam: &Iam, + name: &str, + email: &str, + password: &str, + ) -> anyhow::Result { + api::user::register( + &iam.inner.api, + &api::user::register::Request { + name, + email, + password, + }, + ) + .await?; - User::login(iam, email, password).await + Self::login(iam, email, password).await } - pub async fn login(iam: &Iam, email: &str, password: &str) -> Result { - let client = Client::new(); + pub async fn login(iam: &Iam, email: &str, password: &str) -> anyhow::Result { + let token = api::user::login( + &iam.inner.api, + &api::user::login::Request { email, password }, + ) + .await? + .token; - #[derive(Debug, Deserialize)] - struct Response { - token: String, - } - - let res = client - .post(iam.get_url("/v1/users/login")) - .json(&json!({ - "email": email, - "password": password, - })) - .send() - .await? - .json::>() - .await?; - - let res = unwrap_res(res)?; + let api = iam.inner.api.with_token(token.clone()); Ok(Self { inner: Arc::new(UserInner { - token: res.token.clone(), + token: token.clone(), id: serde_json::from_str::( format!( // HACK: impl FromStr "\"{}\"", jsonwebtoken::decode::( - res.token.as_str(), + token.as_str(), &DecodingKey::from_secret(&[]), &{ let mut v = Validation::new(Algorithm::RS256); @@ -84,7 +74,7 @@ impl User { .as_str(), ) .unwrap(), - _iam: iam.clone(), + _api: api, }), }) } diff --git a/libiam/src/utils.rs b/libiam/src/utils.rs deleted file mode 100644 index 574110b..0000000 --- a/libiam/src/utils.rs +++ /dev/null @@ -1,25 +0,0 @@ -use reqwest::{ - header::{HeaderMap, HeaderValue, AUTHORIZATION}, - Client, -}; -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -pub enum Either { - Left(L), - Right(R), -} - -pub fn create_client(token: &str) -> Client { - let mut header_map = HeaderMap::with_capacity(1); - header_map.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), - ); - - Client::builder() - .default_headers(header_map) - .build() - .expect("failed to create reqwest client") -}