1use bytes::Bytes;
6use http_request_derive::HttpRequest;
7use http_request_derive_client::Client as _;
8use http_request_derive_client_reqwest::{ReqwestClient, ReqwestClientError};
9use http_request_derive_logging::HttpLogger;
10use itertools::Itertools as _;
11use opentalk_client_requests_api_v1::{auth::LoginGetRequest, response::ApiError};
12use opentalk_types_api_v1::auth::{GetLoginResponseBody, OidcProvider};
13use serde::{Deserialize, Serialize};
14use snafu::{ResultExt as _, Snafu, ensure};
15use url::Url;
16
17use crate::{
18 AuthenticatedClient, Authorization,
19 oidc::{OidcEndpoints, OidcWellKnownRequest},
20};
21
22const COMPATIBLE_VERSIONS: &[&str] = &["v1"];
23
24#[derive(Debug, Snafu)]
26pub enum ClientError {
27 #[snafu(display("Reqwest returned an error"))]
35 Reqwest {
36 source: ReqwestClientError,
38 },
39
40 #[snafu(display("The API server returned an error"))]
43 Api {
44 source: ApiError,
46 },
47
48 #[snafu(display(
50 "No compatible API version found under the well-known API endpoint {url}. This client is compatible with API versions: {compatible_versions}."
51 ))]
52 NoCompatibleApiVersion {
53 url: Url,
55
56 compatible_versions: String,
58 },
59
60 #[snafu(display("Invalid OIDC url found: {url:?}"))]
62 InvalidOidcUrl {
63 url: String,
65
66 source: url::ParseError,
68 },
69
70 #[snafu(display(
74 "Discovered url {url} which cannot be a base and therefore is not a valid controller API url"
75 ))]
76 InvalidUrlDiscovered {
77 url: Url,
79 },
80}
81
82impl From<ReqwestClientError> for ClientError {
83 fn from(source: ReqwestClientError) -> Self {
84 Self::Reqwest { source }
85 }
86}
87
88impl From<ApiError> for ClientError {
89 fn from(source: ApiError) -> Self {
90 Self::Api { source }
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct Client {
97 inner: ReqwestClient,
98 #[allow(unused)]
99 oidc_url: Url,
100 #[allow(unused)]
101 api_url: Url,
102}
103
104impl Client {
105 pub async fn discover(url: Url) -> Result<Self, ClientError> {
107 Self::discover_inner(ReqwestClient::new(url)).await
108 }
109
110 pub async fn discover_with_logger(url: Url, logger: HttpLogger) -> Result<Self, ClientError> {
115 Self::discover_inner(ReqwestClient::new(url).with_logger(logger)).await
116 }
117
118 async fn discover_inner(mut client: ReqwestClient) -> Result<Self, ClientError> {
119 match client
120 .execute(WellKnownFrontendRequest)
121 .await
122 .context(ReqwestSnafu)?
123 {
124 WellKnownFrontendResponse::Found(WellKnownFrontendBody {
125 opentalk_controller: ControllerBaseInfo { base_url },
126 }) => {
127 client.set_base_url(base_url);
128 }
129 WellKnownFrontendResponse::NotFound => {}
130 };
131 Self::discover_controller_inner(client).await
132 }
133
134 pub async fn discover_controller(url: Url) -> Result<Self, ClientError> {
136 Self::discover_controller_inner(ReqwestClient::new(url)).await
137 }
138
139 pub async fn discover_controller_with_logger(
144 url: Url,
145 logger: HttpLogger,
146 ) -> Result<Self, ClientError> {
147 Self::discover_controller_inner(ReqwestClient::new(url).with_logger(logger)).await
148 }
149
150 async fn discover_controller_inner(mut client: ReqwestClient) -> Result<Self, ClientError> {
151 let WellKnownApiBody {
152 opentalk_api: ApiInfo { v1 },
153 } = client
154 .execute(WellKnownApiRequest)
155 .await
156 .context(ReqwestSnafu)?;
157
158 let Some(VersionedApiInfo { base_url }) = v1 else {
159 return NoCompatibleApiVersionSnafu {
160 url: client.base_url().clone(),
161 compatible_versions: COMPATIBLE_VERSIONS.iter().join(", "),
162 }
163 .fail();
164 };
165
166 let api_url = match Url::parse(&base_url) {
167 Ok(url) => {
168 ensure!(!url.cannot_be_a_base(), InvalidUrlDiscoveredSnafu { url });
169 url
170 }
171 Err(_e) => {
172 let segments = base_url.trim_start_matches('/');
173 let url = client.base_url().clone();
174 let mut url = url;
175 _ = url.path_segments_mut().unwrap().push(segments);
176 url
177 }
178 };
179
180 client.set_base_url(api_url.clone());
181
182 let GetLoginResponseBody { oidc } = client
183 .execute(LoginGetRequest)
184 .await
185 .context(ReqwestSnafu)?;
186
187 let oidc_url = oidc
188 .url
189 .parse()
190 .context(InvalidOidcUrlSnafu { url: oidc.url })?;
191
192 Ok(Self {
193 oidc_url,
194 api_url,
195 inner: client,
196 })
197 }
198
199 pub async fn get_oidc_endpoints(&self) -> Result<OidcEndpoints, ClientError> {
201 let oidc_client = self.inner.clone().with_base_url(self.oidc_url.clone());
202 let oidc_endpoints = oidc_client
203 .execute(OidcWellKnownRequest)
204 .await
205 .context(ReqwestSnafu)?;
206 Ok(oidc_endpoints)
207 }
208
209 pub async fn get_oidc_provider(&self) -> Result<OidcProvider, ClientError> {
211 let GetLoginResponseBody { oidc } = self
212 .inner
213 .execute(LoginGetRequest)
214 .await
215 .context(ReqwestSnafu)?;
216 Ok(oidc)
217 }
218
219 pub async fn execute<R: HttpRequest + Send>(
221 &self,
222 request: R,
223 ) -> Result<R::Response, ReqwestClientError> {
224 self.inner.execute(request).await
225 }
226
227 pub async fn execute_authorized<R: HttpRequest + Send, A: Authorization + Sync>(
229 &self,
230 request: R,
231 authorization: A,
232 ) -> Result<R::Response, ReqwestClientError> {
233 let authenticated_client = AuthenticatedClient::new(self.inner.clone(), authorization);
234 authenticated_client.execute(request).await
235 }
236
237 }
239
240#[derive(Debug, Clone, PartialEq, Eq, HttpRequest)]
241#[http_request(method="GET", response = WellKnownFrontendResponse, path=".well-known/opentalk/client")]
242struct WellKnownFrontendRequest;
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245struct ControllerBaseInfo {
246 pub base_url: Url,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250struct WellKnownFrontendBody {
251 pub opentalk_controller: ControllerBaseInfo,
252}
253
254enum WellKnownFrontendResponse {
255 NotFound,
256 Found(WellKnownFrontendBody),
257}
258
259impl http_request_derive::FromHttpResponse for WellKnownFrontendResponse {
260 fn from_http_response(
261 http_response: http::Response<Bytes>,
262 ) -> Result<Self, http_request_derive::Error>
263 where
264 Self: Sized,
265 {
266 match <WellKnownFrontendBody as http_request_derive::FromHttpResponse>::from_http_response(
267 http_response,
268 ) {
269 Ok(body) => Ok(Self::Found(body)),
270 Err(e) if e.is_not_found() => Ok(Self::NotFound),
271 Err(e) => Err(e),
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, HttpRequest)]
277#[http_request(method="GET", response = WellKnownApiBody, path=".well-known/opentalk/api")]
278struct WellKnownApiRequest;
279
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281struct VersionedApiInfo {
282 pub base_url: String,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286struct ApiInfo {
287 pub v1: Option<VersionedApiInfo>,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291struct WellKnownApiBody {
292 pub opentalk_api: ApiInfo,
293}