dbx_tools_auth/
oauth_template.rs1use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
2
3const BRAND_YAML: &str = include_str!("../assets/brand.yaml");
4const DEFAULT_CALLBACK_IMAGE: &[u8] = include_bytes!("../assets/logo-light.svg");
5
6pub fn default_callback_image_src() -> String {
8 format!(
9 "data:image/svg+xml;base64,{}",
10 BASE64_STANDARD.encode(DEFAULT_CALLBACK_IMAGE),
11 )
12}
13
14#[derive(Clone, Debug)]
16pub struct OAuthTemplate {
17 brand: CallbackBrand,
18 image_src: String,
19}
20
21#[derive(Clone, Debug)]
22struct CallbackBrand {
23 name: &'static str,
24 tagline: &'static str,
25 primary: &'static str,
26 primary_hover: &'static str,
27 foreground: &'static str,
28 background: &'static str,
29 surface: &'static str,
30 muted: &'static str,
31 border: &'static str,
32 font_family: &'static str,
33}
34
35pub struct OAuthTemplateContext<'a> {
37 pub host: Option<&'a str>,
39 pub error: Option<&'a str>,
41 pub error_description: Option<&'a str>,
43}
44
45impl OAuthTemplate {
46 pub fn new(image_src: Option<String>) -> Self {
48 Self {
49 brand: CallbackBrand::load(),
50 image_src: image_src.unwrap_or_else(default_callback_image_src),
51 }
52 }
53
54 pub fn render(&self, context: OAuthTemplateContext<'_>) -> String {
56 let page_title = context
57 .error
58 .map(title)
59 .unwrap_or_else(|| "Success".to_owned());
60 let result = if context.error.is_some() {
61 format!(
62 r#"<div class="title">{}</div><div class="content">{}</div>"#,
63 escape_html(&page_title),
64 escape_html(context.error_description.unwrap_or_default()),
65 )
66 } else {
67 let host = context.host.map_or_else(String::new, |host| {
68 let host = escape_html(host);
69 format!(r#"<div class="content">Go to <a href="{host}">{host}</a></div>"#)
70 });
71 format!(r#"<div class="title">Authenticated</div>{host}"#)
72 };
73 let image_src = escape_html(&self.image_src);
74 let page_title = escape_html(&page_title);
75 let brand_name = escape_html(self.brand.name);
76 let tagline = escape_html(self.brand.tagline);
77 let primary = self.brand.primary;
78 let primary_hover = self.brand.primary_hover;
79 let foreground = self.brand.foreground;
80 let background = self.brand.background;
81 let surface = self.brand.surface;
82 let muted = self.brand.muted;
83 let border = self.brand.border;
84 let font_family = self.brand.font_family;
85
86 format!(
87 r#"<!doctype html>
88<html lang="en">
89 <head>
90 <meta charset="utf-8">
91 <meta name="viewport" content="width=device-width, initial-scale=1">
92 <title>{page_title}</title>
93 <style>
94 html, body {{ height: 100%; }}
95 body {{
96 margin: 0;
97 background: {surface};
98 color: {foreground};
99 font-family: {font_family};
100 }}
101 .root-container {{
102 display: flex;
103 min-height: 100%;
104 align-items: center;
105 justify-content: center;
106 }}
107 .info-container {{
108 display: flex;
109 width: min(320px, calc(100vw - 96px));
110 flex-direction: column;
111 align-items: center;
112 gap: 24px;
113 padding: 48px;
114 border: 1px solid {border};
115 border-radius: 12px;
116 background: {background};
117 box-shadow: 0 8px 25px rgba(27, 49, 57, 0.12);
118 text-align: center;
119 }}
120 .brand {{ display: block; width: 360px; max-width: calc(100vw - 96px); height: auto; margin: 0 auto; transform: translateX(8%); }}
121 .tagline {{ color: {muted}; font-size: 13px; line-height: 18px; }}
122 .title {{ color: {primary}; font-size: 24px; font-weight: 700; line-height: 28px; }}
123 .content {{ width: 100%; color: {muted}; font-size: 14px; line-height: 20px; }}
124 a {{ color: {primary}; }}
125 a:hover {{ color: {primary_hover}; }}
126 </style>
127 </head>
128 <body>
129 <main class="root-container">
130 <section class="info-container">
131 <img class="brand" src="{image_src}" alt="{brand_name}">
132 <div class="tagline">{tagline}</div>
133 {result}
134 <div class="content">You can close this tab.</div>
135 </section>
136 </main>
137 </body>
138</html>"#
139 )
140 }
141}
142
143impl Default for OAuthTemplate {
144 fn default() -> Self {
145 Self::new(None)
146 }
147}
148
149impl CallbackBrand {
150 fn load() -> Self {
151 Self {
152 name: brand_value("name").unwrap_or("dbx tools"),
153 tagline: brand_value("tagline").unwrap_or("Practical tools for Databricks builders."),
154 primary: brand_value("primary").unwrap_or("#1B3139"),
155 primary_hover: brand_value("primaryHover").unwrap_or("#0E538B"),
156 foreground: brand_value("foreground").unwrap_or("#1B3139"),
157 background: brand_value("background").unwrap_or("#FFFFFF"),
158 surface: brand_value("surface").unwrap_or("#F9F7F4"),
159 muted: brand_value("muted").unwrap_or("#618794"),
160 border: brand_value("border").unwrap_or("#E4E2DD"),
161 font_family: brand_value("sans")
162 .unwrap_or("'DM Sans', ui-sans-serif, system-ui, sans-serif"),
163 }
164 }
165}
166
167fn brand_value(key: &str) -> Option<&'static str> {
168 BRAND_YAML.lines().find_map(|line| {
169 let (candidate, value) = line.trim().split_once(':')?;
170 if candidate != key {
171 return None;
172 }
173 let value = value
174 .split_once(" #")
175 .map_or(value, |(value, _)| value)
176 .trim()
177 .trim_matches('"');
178 (!value.is_empty()).then_some(value)
179 })
180}
181
182fn title(value: &str) -> String {
183 value
184 .split(['_', ' '])
185 .filter(|word| !word.is_empty())
186 .map(|word| {
187 let mut characters = word.chars();
188 match characters.next() {
189 Some(first) => first.to_uppercase().collect::<String>() + characters.as_str(),
190 None => String::new(),
191 }
192 })
193 .collect::<Vec<_>>()
194 .join(" ")
195}
196
197fn escape_html(value: &str) -> String {
198 let mut escaped = String::with_capacity(value.len());
199 for character in value.chars() {
200 match character {
201 '&' => escaped.push_str("&"),
202 '<' => escaped.push_str("<"),
203 '>' => escaped.push_str(">"),
204 '"' => escaped.push_str("""),
205 '\'' => escaped.push_str("'"),
206 _ => escaped.push(character),
207 }
208 }
209 escaped
210}
211
212#[cfg(test)]
213mod tests {
214 use super::{brand_value, default_callback_image_src, OAuthTemplate, OAuthTemplateContext};
215
216 #[test]
217 fn renders_default_branding_and_success() {
218 let html = OAuthTemplate::default().render(OAuthTemplateContext {
219 host: Some("https://example.com/?a=1&b=2"),
220 error: None,
221 error_description: None,
222 });
223
224 assert!(html.contains(&default_callback_image_src()));
225 assert!(default_callback_image_src().starts_with("data:image/svg+xml;base64,"));
226 assert_eq!(brand_value("primary"), Some("#1B3139"));
227 assert!(html.contains("Practical tools for Databricks builders."));
228 assert!(html.contains("Authenticated"));
229 assert!(html.contains("https://example.com/?a=1&b=2"));
230 }
231
232 #[test]
233 fn renders_custom_branding_and_escaped_error() {
234 let html = OAuthTemplate::new(Some("data:image/svg+xml,&custom".to_owned())).render(
235 OAuthTemplateContext {
236 host: None,
237 error: Some("access_denied"),
238 error_description: Some("<denied>"),
239 },
240 );
241
242 assert!(html.contains(r#"src="data:image/svg+xml,&custom""#));
243 assert!(html.contains("Access Denied"));
244 assert!(html.contains("<denied>"));
245 assert!(!html.contains("Authenticated"));
246 }
247}