mas_router/
url_builder.rs1use ulid::Ulid;
10use url::Url;
11
12use crate::traits::Route;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct UrlBuilder {
16 http_base: Url,
17 prefix: String,
18 assets_base: String,
19 issuer: Url,
20}
21
22impl UrlBuilder {
23 #[must_use]
25 pub fn absolute_url_for<U>(&self, destination: &U) -> Url
26 where
27 U: Route,
28 {
29 destination.absolute_url(&self.http_base)
30 }
31
32 #[must_use]
34 pub fn relative_url_for<U>(&self, destination: &U) -> String
35 where
36 U: Route,
37 {
38 format!(
39 "{prefix}{destination}",
40 prefix = self.prefix,
41 destination = destination.path_and_query()
42 )
43 }
44
45 #[must_use]
47 pub fn prefix(&self) -> Option<&str> {
48 if self.prefix.is_empty() {
49 None
50 } else {
51 Some(&self.prefix)
52 }
53 }
54
55 pub fn redirect<U>(&self, destination: &U) -> axum::response::Redirect
57 where
58 U: Route,
59 {
60 let uri = self.relative_url_for(destination);
61 axum::response::Redirect::to(&uri)
62 }
63
64 pub fn absolute_redirect<U>(&self, destination: &U) -> axum::response::Redirect
66 where
67 U: Route,
68 {
69 let uri = self.absolute_url_for(destination);
70 axum::response::Redirect::to(uri.as_str())
71 }
72
73 #[must_use]
80 pub fn new(base: Url, issuer: Option<Url>, assets_base: Option<String>) -> Self {
81 assert!(
82 base.scheme() == "http" || base.scheme() == "https",
83 "base URL must be HTTP/HTTPS"
84 );
85 assert_eq!(base.query(), None, "base URL must not contain a query");
86 assert_eq!(
87 base.fragment(),
88 None,
89 "base URL must not contain a fragment"
90 );
91 assert_eq!(base.username(), "", "base URL must not contain credentials");
92 assert_eq!(
93 base.password(),
94 None,
95 "base URL must not contain credentials"
96 );
97
98 let issuer = issuer.unwrap_or_else(|| base.clone());
99 let prefix = base.path().trim_end_matches('/').to_owned();
100 let assets_base = assets_base.unwrap_or_else(|| format!("{prefix}/assets/"));
101 Self {
102 http_base: base,
103 prefix,
104 assets_base,
105 issuer,
106 }
107 }
108
109 #[must_use]
115 pub fn public_hostname(&self) -> &str {
116 self.http_base
117 .host_str()
118 .expect("base URL must have a host")
119 }
120
121 #[must_use]
123 pub fn http_base(&self) -> Url {
124 self.http_base.clone()
125 }
126
127 #[must_use]
129 pub fn oidc_issuer(&self) -> Url {
130 self.issuer.clone()
131 }
132
133 #[must_use]
135 pub fn oidc_discovery(&self) -> Url {
136 crate::endpoints::OidcConfiguration.absolute_url(&self.issuer)
137 }
138
139 #[must_use]
141 pub fn oauth_authorization_endpoint(&self) -> Url {
142 self.absolute_url_for(&crate::endpoints::OAuth2AuthorizationEndpoint)
143 }
144
145 #[must_use]
147 pub fn oauth_token_endpoint(&self) -> Url {
148 self.absolute_url_for(&crate::endpoints::OAuth2TokenEndpoint)
149 }
150
151 #[must_use]
153 pub fn oauth_introspection_endpoint(&self) -> Url {
154 self.absolute_url_for(&crate::endpoints::OAuth2Introspection)
155 }
156
157 #[must_use]
159 pub fn oauth_revocation_endpoint(&self) -> Url {
160 self.absolute_url_for(&crate::endpoints::OAuth2Revocation)
161 }
162
163 #[must_use]
165 pub fn oauth_registration_endpoint(&self) -> Url {
166 self.absolute_url_for(&crate::endpoints::OAuth2RegistrationEndpoint)
167 }
168
169 #[must_use]
171 pub fn oauth_device_authorization_endpoint(&self) -> Url {
172 self.absolute_url_for(&crate::endpoints::OAuth2DeviceAuthorizationEndpoint)
173 }
174
175 #[must_use]
177 pub fn device_code_link(&self) -> Url {
178 self.absolute_url_for(&crate::endpoints::DeviceCodeLink::default())
179 }
180
181 #[must_use]
183 pub fn device_code_link_full(&self, code: String) -> Url {
184 self.absolute_url_for(&crate::endpoints::DeviceCodeLink::with_code(code))
185 }
186
187 #[must_use]
189 pub fn oidc_userinfo_endpoint(&self) -> Url {
190 self.absolute_url_for(&crate::endpoints::OidcUserinfo)
191 }
192
193 #[must_use]
195 pub fn jwks_uri(&self) -> Url {
196 self.absolute_url_for(&crate::endpoints::OAuth2Keys)
197 }
198
199 #[must_use]
201 pub fn static_asset(&self, path: String) -> Url {
202 self.absolute_url_for(&crate::endpoints::StaticAsset::new(path))
203 }
204
205 #[must_use]
207 pub fn assets_base(&self) -> &str {
208 &self.assets_base
209 }
210
211 #[must_use]
213 pub fn graphql_endpoint(&self) -> Url {
214 self.absolute_url_for(&crate::endpoints::GraphQL)
215 }
216
217 #[must_use]
219 pub fn upstream_oauth_callback(&self, id: Ulid) -> Url {
220 self.absolute_url_for(&crate::endpoints::UpstreamOAuth2Callback::new(id))
221 }
222
223 #[must_use]
225 pub fn upstream_oauth_authorize(&self, id: Ulid) -> Url {
226 self.absolute_url_for(&crate::endpoints::UpstreamOAuth2Authorize::new(id))
227 }
228
229 #[must_use]
231 pub fn account_management_uri(&self) -> Url {
232 self.absolute_url_for(&crate::endpoints::Account::default())
233 }
234
235 #[must_use]
237 pub fn account_recovery_link(&self, ticket: String) -> Url {
238 self.absolute_url_for(&crate::endpoints::AccountRecoveryFinish::new(ticket))
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 #[test]
245 #[should_panic(expected = "base URL must be HTTP/HTTPS")]
246 fn test_invalid_base_url_scheme() {
247 let _ = super::UrlBuilder::new(url::Url::parse("file:///tmp/").unwrap(), None, None);
248 }
249
250 #[test]
251 #[should_panic(expected = "base URL must not contain a query")]
252 fn test_invalid_base_url_query() {
253 let _ = super::UrlBuilder::new(
254 url::Url::parse("https://example.com/?foo=bar").unwrap(),
255 None,
256 None,
257 );
258 }
259
260 #[test]
261 #[should_panic(expected = "base URL must not contain a fragment")]
262 fn test_invalid_base_url_fragment() {
263 let _ = super::UrlBuilder::new(
264 url::Url::parse("https://example.com/#foo").unwrap(),
265 None,
266 None,
267 );
268 }
269
270 #[test]
271 #[should_panic(expected = "base URL must not contain credentials")]
272 fn test_invalid_base_url_credentials() {
273 let _ = super::UrlBuilder::new(
274 url::Url::parse("https://foo@example.com/").unwrap(),
275 None,
276 None,
277 );
278 }
279
280 #[test]
281 fn test_url_prefix() {
282 let builder = super::UrlBuilder::new(
283 url::Url::parse("https://example.com/foo/").unwrap(),
284 None,
285 None,
286 );
287 assert_eq!(builder.prefix, "/foo");
288
289 let builder =
290 super::UrlBuilder::new(url::Url::parse("https://example.com/").unwrap(), None, None);
291 assert_eq!(builder.prefix, "");
292 }
293
294 #[test]
295 fn test_absolute_uri_prefix() {
296 let builder = super::UrlBuilder::new(
297 url::Url::parse("https://example.com/foo/").unwrap(),
298 None,
299 None,
300 );
301
302 let uri = builder.absolute_url_for(&crate::endpoints::OAuth2AuthorizationEndpoint);
303 assert_eq!(uri.as_str(), "https://example.com/foo/authorize");
304 }
305}