use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrandingConfig {
pub id: Uuid,
pub tenant_id: Uuid,
pub brand_name: String,
pub logo: LogoConfig,
pub theme: ThemeConfig,
pub typography: TypographyConfig,
pub custom_css: Option<String>,
pub email_templates: EmailTemplates,
pub custom_domain: Option<CustomDomain>,
pub footer: FooterConfig,
pub favicon_url: Option<String>,
pub meta_tags: MetaTags,
pub feature_visibility: FeatureVisibility,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl BrandingConfig {
pub fn default_for_tenant(tenant_id: Uuid, brand_name: &str) -> Self {
Self {
id: Uuid::new_v4(),
tenant_id,
brand_name: brand_name.to_string(),
logo: LogoConfig::default(),
theme: ThemeConfig::default(),
typography: TypographyConfig::default(),
custom_css: None,
email_templates: EmailTemplates::default(),
custom_domain: None,
footer: FooterConfig::default(),
favicon_url: None,
meta_tags: MetaTags::default(),
feature_visibility: FeatureVisibility::default(),
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogoConfig {
pub primary_url: Option<String>,
pub dark_mode_url: Option<String>,
pub icon_url: Option<String>,
pub alt_text: String,
pub width: Option<u32>,
pub height: Option<u32>,
}
impl Default for LogoConfig {
fn default() -> Self {
Self {
primary_url: None,
dark_mode_url: None,
icon_url: None,
alt_text: "Logo".to_string(),
width: None,
height: Some(40),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThemeConfig {
pub primary_color: String,
pub secondary_color: String,
pub accent_color: String,
pub background_color: String,
pub surface_color: String,
pub text_color: String,
pub text_secondary_color: String,
pub border_color: String,
pub success_color: String,
pub warning_color: String,
pub error_color: String,
pub info_color: String,
pub dark_mode: Option<Box<ThemeConfig>>,
pub border_radius: String,
pub shadow: String,
}
impl Default for ThemeConfig {
fn default() -> Self {
Self {
primary_color: "#2563eb".to_string(),
secondary_color: "#7c3aed".to_string(),
accent_color: "#06b6d4".to_string(),
background_color: "#ffffff".to_string(),
surface_color: "#f9fafb".to_string(),
text_color: "#111827".to_string(),
text_secondary_color: "#6b7280".to_string(),
border_color: "#e5e7eb".to_string(),
success_color: "#059669".to_string(),
warning_color: "#d97706".to_string(),
error_color: "#dc2626".to_string(),
info_color: "#2563eb".to_string(),
dark_mode: Some(Box::new(Self::dark_mode_defaults())),
border_radius: "0.5rem".to_string(),
shadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)".to_string(),
}
}
}
impl ThemeConfig {
pub fn dark_mode_defaults() -> Self {
Self {
primary_color: "#3b82f6".to_string(),
secondary_color: "#8b5cf6".to_string(),
accent_color: "#22d3ee".to_string(),
background_color: "#0f172a".to_string(),
surface_color: "#1e293b".to_string(),
text_color: "#f1f5f9".to_string(),
text_secondary_color: "#94a3b8".to_string(),
border_color: "#334155".to_string(),
success_color: "#10b981".to_string(),
warning_color: "#f59e0b".to_string(),
error_color: "#ef4444".to_string(),
info_color: "#3b82f6".to_string(),
dark_mode: None,
border_radius: "0.5rem".to_string(),
shadow: "0 1px 3px 0 rgb(0 0 0 / 0.3)".to_string(),
}
}
pub fn to_css_variables(&self) -> String {
format!(
r#":root {{
--color-primary: {};
--color-secondary: {};
--color-accent: {};
--color-background: {};
--color-surface: {};
--color-text: {};
--color-text-secondary: {};
--color-border: {};
--color-success: {};
--color-warning: {};
--color-error: {};
--color-info: {};
--border-radius: {};
--shadow: {};
}}"#,
self.primary_color,
self.secondary_color,
self.accent_color,
self.background_color,
self.surface_color,
self.text_color,
self.text_secondary_color,
self.border_color,
self.success_color,
self.warning_color,
self.error_color,
self.info_color,
self.border_radius,
self.shadow
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypographyConfig {
pub font_family: String,
pub heading_font_family: Option<String>,
pub mono_font_family: String,
pub base_font_size: String,
pub line_height: String,
pub font_weights: FontWeights,
pub custom_font_urls: Vec<String>,
}
impl Default for TypographyConfig {
fn default() -> Self {
Self {
font_family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif".to_string(),
heading_font_family: None,
mono_font_family: "ui-monospace, SFMono-Regular, Menlo, Monaco, monospace".to_string(),
base_font_size: "16px".to_string(),
line_height: "1.5".to_string(),
font_weights: FontWeights::default(),
custom_font_urls: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FontWeights {
pub light: u16,
pub regular: u16,
pub medium: u16,
pub semibold: u16,
pub bold: u16,
}
impl Default for FontWeights {
fn default() -> Self {
Self {
light: 300,
regular: 400,
medium: 500,
semibold: 600,
bold: 700,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailTemplates {
pub welcome: EmailTemplate,
pub invitation: EmailTemplate,
pub password_reset: EmailTemplate,
pub session_shared: EmailTemplate,
pub weekly_digest: EmailTemplate,
pub custom: HashMap<String, EmailTemplate>,
}
impl Default for EmailTemplates {
fn default() -> Self {
Self {
welcome: EmailTemplate::default_welcome(),
invitation: EmailTemplate::default_invitation(),
password_reset: EmailTemplate::default_password_reset(),
session_shared: EmailTemplate::default_session_shared(),
weekly_digest: EmailTemplate::default_digest(),
custom: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailTemplate {
pub subject: String,
pub body_html: String,
pub body_text: String,
pub from_name: Option<String>,
pub reply_to: Option<String>,
}
impl EmailTemplate {
fn default_welcome() -> Self {
Self {
subject: "Welcome to {{brand_name}}".to_string(),
body_html: r#"<h1>Welcome to {{brand_name}}</h1><p>Hello {{user_name}},</p><p>Your account has been created.</p>"#.to_string(),
body_text: "Welcome to {{brand_name}}\n\nHello {{user_name}},\nYour account has been created.".to_string(),
from_name: None,
reply_to: None,
}
}
fn default_invitation() -> Self {
Self {
subject: "You've been invited to {{brand_name}}".to_string(),
body_html: r#"<h1>You're invited!</h1><p>{{inviter_name}} has invited you to join {{brand_name}}.</p>"#.to_string(),
body_text: "You're invited!\n\n{{inviter_name}} has invited you to join {{brand_name}}.".to_string(),
from_name: None,
reply_to: None,
}
}
fn default_password_reset() -> Self {
Self {
subject: "Reset your {{brand_name}} password".to_string(),
body_html: r#"<h1>Password Reset</h1><p>Click the link below to reset your password.</p>"#.to_string(),
body_text: "Password Reset\n\nClick the link below to reset your password.".to_string(),
from_name: None,
reply_to: None,
}
}
fn default_session_shared() -> Self {
Self {
subject: "{{sharer_name}} shared a session with you".to_string(),
body_html: r#"<h1>Session Shared</h1><p>{{sharer_name}} shared "{{session_title}}" with you.</p>"#.to_string(),
body_text: "Session Shared\n\n{{sharer_name}} shared \"{{session_title}}\" with you.".to_string(),
from_name: None,
reply_to: None,
}
}
fn default_digest() -> Self {
Self {
subject: "Your weekly {{brand_name}} digest".to_string(),
body_html: r#"<h1>Weekly Digest</h1><p>Here's your activity summary.</p>"#.to_string(),
body_text: "Weekly Digest\n\nHere's your activity summary.".to_string(),
from_name: None,
reply_to: None,
}
}
pub fn render(&self, variables: &HashMap<String, String>) -> (String, String, String) {
let mut subject = self.subject.clone();
let mut body_html = self.body_html.clone();
let mut body_text = self.body_text.clone();
for (key, value) in variables {
let placeholder = format!("{{{{{}}}}}", key);
subject = subject.replace(&placeholder, value);
body_html = body_html.replace(&placeholder, value);
body_text = body_text.replace(&placeholder, value);
}
(subject, body_html, body_text)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomDomain {
pub domain: String,
pub ssl_status: SslStatus,
pub dns_verified: bool,
pub dns_token: String,
pub cname_target: String,
pub configured_at: DateTime<Utc>,
pub ssl_expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SslStatus {
Pending,
Provisioning,
Active,
Failed,
Expired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FooterConfig {
pub show_powered_by: bool,
pub powered_by_text: Option<String>,
pub copyright_text: Option<String>,
pub links: Vec<FooterLink>,
pub social_links: Vec<SocialLink>,
}
impl Default for FooterConfig {
fn default() -> Self {
Self {
show_powered_by: true,
powered_by_text: None,
copyright_text: None,
links: vec![],
social_links: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FooterLink {
pub label: String,
pub url: String,
pub new_tab: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocialLink {
pub platform: SocialPlatform,
pub url: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SocialPlatform {
Twitter,
LinkedIn,
GitHub,
Facebook,
Instagram,
YouTube,
Discord,
Slack,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetaTags {
pub title_template: String,
pub description: String,
pub keywords: Vec<String>,
pub og_image_url: Option<String>,
pub twitter_card: String,
pub custom: HashMap<String, String>,
}
impl Default for MetaTags {
fn default() -> Self {
Self {
title_template: "{{page_title}} | {{brand_name}}".to_string(),
description: "AI chat session management platform".to_string(),
keywords: vec![],
og_image_url: None,
twitter_card: "summary_large_image".to_string(),
custom: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureVisibility {
pub show_provider_logos: bool,
pub show_analytics: bool,
pub show_collaboration: bool,
pub show_export: bool,
pub show_api_docs: bool,
pub show_help: bool,
pub hidden_features: Vec<String>,
}
impl Default for FeatureVisibility {
fn default() -> Self {
Self {
show_provider_logos: true,
show_analytics: true,
show_collaboration: true,
show_export: true,
show_api_docs: true,
show_help: true,
hidden_features: vec![],
}
}
}
pub struct BrandingManager {
configs: HashMap<Uuid, BrandingConfig>,
}
impl BrandingManager {
pub fn new() -> Self {
Self {
configs: HashMap::new(),
}
}
pub fn create_branding(&mut self, tenant_id: Uuid, brand_name: &str) -> BrandingConfig {
let config = BrandingConfig::default_for_tenant(tenant_id, brand_name);
self.configs.insert(tenant_id, config.clone());
config
}
pub fn get_branding(&self, tenant_id: Uuid) -> Option<&BrandingConfig> {
self.configs.get(&tenant_id)
}
pub fn update_theme(&mut self, tenant_id: Uuid, theme: ThemeConfig) -> bool {
if let Some(config) = self.configs.get_mut(&tenant_id) {
config.theme = theme;
config.updated_at = Utc::now();
true
} else {
false
}
}
pub fn update_logo(&mut self, tenant_id: Uuid, logo: LogoConfig) -> bool {
if let Some(config) = self.configs.get_mut(&tenant_id) {
config.logo = logo;
config.updated_at = Utc::now();
true
} else {
false
}
}
pub fn set_custom_domain(&mut self, tenant_id: Uuid, domain: &str) -> Option<CustomDomain> {
let config = self.configs.get_mut(&tenant_id)?;
let custom_domain = CustomDomain {
domain: domain.to_string(),
ssl_status: SslStatus::Pending,
dns_verified: false,
dns_token: Uuid::new_v4().to_string(),
cname_target: "app.chasm.cloud".to_string(),
configured_at: Utc::now(),
ssl_expires_at: None,
};
config.custom_domain = Some(custom_domain.clone());
config.updated_at = Utc::now();
Some(custom_domain)
}
pub fn generate_css(&self, tenant_id: Uuid) -> Option<String> {
let config = self.configs.get(&tenant_id)?;
let mut css = config.theme.to_css_variables();
if let Some(ref custom_css) = config.custom_css {
css.push_str("\n\n/* Custom CSS */\n");
css.push_str(custom_css);
}
Some(css)
}
}
impl Default for BrandingManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_theme_css_variables() {
let theme = ThemeConfig::default();
let css = theme.to_css_variables();
assert!(css.contains("--color-primary"));
assert!(css.contains("#2563eb"));
}
#[test]
fn test_email_template_render() {
let template = EmailTemplate::default_welcome();
let mut vars = HashMap::new();
vars.insert("brand_name".to_string(), "Acme".to_string());
vars.insert("user_name".to_string(), "John".to_string());
let (subject, _, _) = template.render(&vars);
assert_eq!(subject, "Welcome to Acme");
}
#[test]
fn test_branding_manager() {
let mut manager = BrandingManager::new();
let tenant_id = Uuid::new_v4();
let config = manager.create_branding(tenant_id, "Test Brand");
assert_eq!(config.brand_name, "Test Brand");
assert!(manager.get_branding(tenant_id).is_some());
}
}