Skip to main content

allowthem_server/
branding.rs

1use allowthem_core::AllowThem;
2use allowthem_core::applications::BrandingConfig;
3use allowthem_core::types::ClientId;
4
5/// Compute accent color variants from a primary hex color.
6///
7/// Returns `(accent, accent_hover, accent_ring)` as hex strings.
8/// Falls back to default blue values if the hex string cannot be parsed.
9pub fn compute_accent_variants(hex: &str) -> (String, String, String) {
10    match parse_hex(hex) {
11        Some((r, g, b)) => {
12            let accent = format!("#{:02x}{:02x}{:02x}", r, g, b);
13            let hover = darken(r, g, b, 0.15);
14            let ring = lighten(r, g, b, 0.25);
15            (accent, hover, ring)
16        }
17        None => default_accents(),
18    }
19}
20
21pub fn default_accents() -> (String, String, String) {
22    ("#2563eb".into(), "#1d4ed8".into(), "#3b82f6".into())
23}
24
25/// Look up branding for an application by client_id.
26///
27/// Returns `None` for missing or inactive applications.
28/// Logs a warning on unexpected DB errors and falls back to `None`.
29pub async fn lookup_branding(
30    ath: &AllowThem,
31    client_id: Option<&ClientId>,
32) -> Option<BrandingConfig> {
33    let cid = client_id?;
34    match ath.db().get_branding_by_client_id(cid).await {
35        Ok(branding) => branding,
36        Err(e) => {
37            tracing::warn!(client_id = %cid, error = %e, "branding lookup failed");
38            None
39        }
40    }
41}
42
43fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> {
44    let bytes = hex.as_bytes();
45    if bytes.len() != 7 || bytes[0] != b'#' {
46        return None;
47    }
48    let r = u8::from_str_radix(&hex[1..3], 16).ok()?;
49    let g = u8::from_str_radix(&hex[3..5], 16).ok()?;
50    let b = u8::from_str_radix(&hex[5..7], 16).ok()?;
51    Some((r, g, b))
52}
53
54fn darken(r: u8, g: u8, b: u8, factor: f32) -> String {
55    let scale = 1.0 - factor;
56    format!(
57        "#{:02x}{:02x}{:02x}",
58        (r as f32 * scale) as u8,
59        (g as f32 * scale) as u8,
60        (b as f32 * scale) as u8,
61    )
62}
63
64fn lighten(r: u8, g: u8, b: u8, factor: f32) -> String {
65    format!(
66        "#{:02x}{:02x}{:02x}",
67        (r as f32 + (255.0 - r as f32) * factor) as u8,
68        (g as f32 + (255.0 - g as f32) * factor) as u8,
69        (b as f32 + (255.0 - b as f32) * factor) as u8,
70    )
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn parse_hex_valid() {
79        assert_eq!(compute_accent_variants("#2563eb").0, "#2563eb");
80    }
81
82    #[test]
83    fn default_accents_are_blue() {
84        let (accent, _, _) = default_accents();
85        assert_eq!(accent, "#2563eb");
86    }
87
88    #[test]
89    fn compute_variants_invalid_falls_back() {
90        assert_eq!(compute_accent_variants("invalid"), default_accents());
91    }
92}