Branding and templates: per-domain, tenant and server logos, /logo, operator calendar email templates and RSVP page (BT-1 to BT-26)

Logos resolve domain, then tenant, then server-wide, then the built-in, with
subdomains finding their domain. GET /logo serves a data-URL image, redirects
to a URL logo without fetching it, sandboxes SVG, and answers 404 when no
custom logo applies. Emails embed the first PNG, JPEG or GIF logo. Logo and
template writes are checked; stored templates are read at send time, always
escaped, and fall back to the built-in with a build warning when they don't
parse. The RSVP page is served byte for byte with a CSP and no-referrer. The
sign-in and RSVP pages load the logo through an image element. MT-22's
session logo follows the chain to the server-wide logo.
Acceptance tests 1 to 17; test 18 written as the ignored branding_compat.
This commit is contained in:
2026-09-18 22:27:19 -07:00
parent ecbdfd533b
commit 0bc6b03dcd
29 changed files with 1772 additions and 91 deletions
+15 -24
View File
@@ -8,9 +8,10 @@
//!
//! Its domain's logo if set, else its tenant's. The value is returned as
//! stored, a URL or a data URL: the server never fetches a logo URL itself
//! (MT-23). Branding extends the chain past the tenant (BT-1).
//! (MT-23). Branding extends the chain past the tenant to the server-wide
//! logo (BT-2); none means the client's built-in INBUXA logo.
use registry::schema::structs::{Account, Domain, Tenant};
use registry::schema::structs::{Account, Domain, Enterprise, Tenant};
use store::RegistryStore;
use types::id::Id;
@@ -28,29 +29,19 @@ pub async fn for_account(registry: &RegistryStore, account_id: u32) -> trc::Resu
Some(tenant_id) => registry.object::<Tenant>(tenant_id).await?,
None => None,
};
Ok(applicable(
// BT-2: past the tenant, the server-wide logo; each value as stored, and
// an unusable one skipped (BT-4)
let server = registry
.object::<Enterprise>(Id::singleton())
.await?
.and_then(|e| e.logo_url);
Ok([
domain.as_ref().and_then(|d| d.logo.as_deref()),
tenant.as_ref().and_then(|t| t.logo.as_deref()),
)
server.as_deref(),
]
.into_iter()
.flatten()
.find(|value| crate::branding::logo::read(value).is_some())
.map(str::to_string))
}
/// The logo that applies, from the principal's domain's and tenant's logos.
pub fn applicable<'x>(domain: Option<&'x str>, tenant: Option<&'x str>) -> Option<&'x str> {
domain
.filter(|logo| !logo.is_empty())
.or(tenant.filter(|logo| !logo.is_empty()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn domain_then_tenant() {
assert_eq!(applicable(Some("d"), Some("t")), Some("d"));
assert_eq!(applicable(None, Some("t")), Some("t"));
assert_eq!(applicable(Some(""), Some("t")), Some("t"));
assert_eq!(applicable(None, None), None);
}
}