1use bytes::Bytes;
6use http_request_derive::HttpRequest;
7use http_request_derive_client::Client as _;
8use http_request_derive_client_reqwest::{ReqwestClient, ReqwestClientError};
9use itertools::Itertools as _;
10use opentalk_client_requests_api_v1::{auth::LoginGetRequest, response::ApiError};
11use opentalk_types_api_v1::auth::{GetLoginResponseBody, OidcProvider};
12use serde::{Deserialize, Serialize};
13use snafu::{ResultExt as _, Snafu, ensure};
14use url::Url;
15
16use crate::{
17 AuthenticatedClient, Authorization,
18 oidc::{OidcEndpoints, OidcWellKnownRequest},
19};
20
21const COMPATIBLE_VERSIONS: &[&str] = &["v1"];
22
23#[derive(Debug, Snafu)]
24pub enum ClientError {
25 #[snafu(display("Reqwest returned an error"))]
26 Reqwest { source: ReqwestClientError },
27
28 #[snafu(display("The API server returned an error"))]
29 Api { source: ApiError },
30
31 #[snafu(display(
32 "No compatible API version found under the well-known API endpoint {url}. This client is compatible with API versions: {compatible_versions}."
33 ))]
34 NoCompatibleApiVersion {
35 url: Url,
36 compatible_versions: String,
37 },
38
39 #[snafu(display("Invalid OIDC url found: {url:?}"))]
40 InvalidOidcUrl {
41 url: String,
42 source: url::ParseError,
43 },
44
45 #[snafu(display(
46 "Discovered url {url} which cannot be a base and therefore is not a valid controller API url"
47 ))]
48 InvalidUrlDiscovered { url: Url },
49}
50
51#[derive(Debug, Clone)]
53pub struct Client {
54 inner: ReqwestClient,
55 #[allow(unused)]
56 oidc_url: Url,
57 #[allow(unused)]
58 api_url: Url,
59}
60
61impl Client {
62 pub async fn discover(url: Url) -> Result<Self, ClientError> {
64 let discovery_client = ReqwestClient::new(url.clone());
65
66 match discovery_client
67 .execute(WellKnownFrontendRequest)
68 .await
69 .context(ReqwestSnafu)?
70 {
71 WellKnownFrontendResponse::Found(WellKnownFrontendBody {
72 opentalk_controller: ControllerBaseInfo { base_url },
73 }) => Self::discover_controller(base_url).await,
74 WellKnownFrontendResponse::NotFound => Self::discover_controller(url).await,
75 }
76 }
77
78 pub async fn discover_controller(url: Url) -> Result<Self, ClientError> {
80 let discovery_client = ReqwestClient::new(url.clone());
81
82 let WellKnownApiBody {
83 opentalk_api: ApiInfo { v1 },
84 } = discovery_client
85 .execute(WellKnownApiRequest)
86 .await
87 .context(ReqwestSnafu)?;
88
89 let Some(VersionedApiInfo { base_url }) = v1 else {
90 return NoCompatibleApiVersionSnafu {
91 url,
92 compatible_versions: COMPATIBLE_VERSIONS.iter().join(", "),
93 }
94 .fail();
95 };
96
97 let api_url = match Url::parse(&base_url) {
98 Ok(url) => {
99 ensure!(!url.cannot_be_a_base(), InvalidUrlDiscoveredSnafu { url });
100 url
101 }
102 Err(_e) => {
103 let segments = base_url.trim_start_matches('/');
104 let mut url = url;
105 _ = url.path_segments_mut().unwrap().push(segments);
106 url
107 }
108 };
109
110 let inner = ReqwestClient::new(api_url.clone());
111
112 let GetLoginResponseBody { oidc } =
113 inner.execute(LoginGetRequest).await.context(ReqwestSnafu)?;
114
115 let oidc_url = oidc
116 .url
117 .parse()
118 .context(InvalidOidcUrlSnafu { url: oidc.url })?;
119
120 Ok(Self {
121 oidc_url,
122 api_url,
123 inner,
124 })
125 }
126
127 pub async fn get_oidc_endpoints(&self) -> Result<OidcEndpoints, ClientError> {
129 let oidc_client = ReqwestClient::new(self.oidc_url.clone());
130 let oidc_endpoints = oidc_client
131 .execute(OidcWellKnownRequest)
132 .await
133 .context(ReqwestSnafu)?;
134 Ok(oidc_endpoints)
135 }
136
137 pub async fn get_oidc_provider(&self) -> Result<OidcProvider, ClientError> {
139 let GetLoginResponseBody { oidc } = self
140 .inner
141 .execute(LoginGetRequest)
142 .await
143 .context(ReqwestSnafu)?;
144 Ok(oidc)
145 }
146
147 pub async fn execute<R: HttpRequest + Send>(
149 &self,
150 request: R,
151 ) -> Result<R::Response, ReqwestClientError> {
152 self.inner.execute(request).await
153 }
154
155 pub async fn execute_authorized<R: HttpRequest + Send, A: Authorization + Sync>(
157 &self,
158 request: R,
159 authorization: A,
160 ) -> Result<R::Response, ReqwestClientError> {
161 let authenticated_client = AuthenticatedClient::new(self.inner.clone(), authorization);
162 authenticated_client.execute(request).await
163 }
164
165 }
167
168#[derive(Debug, Clone, PartialEq, Eq, HttpRequest)]
169#[http_request(method="GET", response = WellKnownFrontendResponse, path=".well-known/opentalk/client")]
170struct WellKnownFrontendRequest;
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173struct ControllerBaseInfo {
174 pub base_url: Url,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178struct WellKnownFrontendBody {
179 pub opentalk_controller: ControllerBaseInfo,
180}
181
182enum WellKnownFrontendResponse {
183 NotFound,
184 Found(WellKnownFrontendBody),
185}
186
187impl http_request_derive::FromHttpResponse for WellKnownFrontendResponse {
188 fn from_http_response(
189 http_response: http::Response<Bytes>,
190 ) -> Result<Self, http_request_derive::Error>
191 where
192 Self: Sized,
193 {
194 match <WellKnownFrontendBody as http_request_derive::FromHttpResponse>::from_http_response(
195 http_response,
196 ) {
197 Ok(body) => Ok(Self::Found(body)),
198 Err(e) if e.is_not_found() => Ok(Self::NotFound),
199 Err(e) => Err(e),
200 }
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, HttpRequest)]
205#[http_request(method="GET", response = WellKnownApiBody, path=".well-known/opentalk/api")]
206struct WellKnownApiRequest;
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209struct VersionedApiInfo {
210 pub base_url: String,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214struct ApiInfo {
215 pub v1: Option<VersionedApiInfo>,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219struct WellKnownApiBody {
220 pub opentalk_api: ApiInfo,
221}