feat: add configurable logo to login page with light/dark mode support
- Display Bulwark logo on the login page, with theme-aware switching between light and dark variants based on the resolved theme - Add LOGIN_LOGO_LIGHT_URL and LOGIN_LOGO_DARK_URL env variables to allow custom logo overrides (defaults to bundled Bulwark branding) - Expose logo config through /api/config endpoint and useConfig hook - Add logo URL prompts to the setup.sh installer branding step - Rename branding assets to use underscores instead of spaces
@@ -76,8 +76,16 @@ JMAP_SERVER_URL=https://your-jmap-server.com
|
|||||||
# Login Page Customization (all optional)
|
# Login Page Customization (all optional)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
# Custom logo images for the login page (PNG, SVG, etc.)
|
||||||
|
# Can be absolute URLs or paths relative to the public/ directory.
|
||||||
|
# Light mode logo (shown on light backgrounds), defaults to Bulwark logo.
|
||||||
|
LOGIN_LOGO_LIGHT_URL=/branding/Bulwark_Logo_Color.svg
|
||||||
|
#
|
||||||
|
# Dark mode logo (shown on dark backgrounds), defaults to Bulwark white logo.
|
||||||
|
LOGIN_LOGO_DARK_URL=/branding/Bulwark_Logo_Color.svg
|
||||||
|
|
||||||
# Company or organization name displayed above the version on the login page
|
# Company or organization name displayed above the version on the login page
|
||||||
# LOGIN_COMPANY_NAME=My Company
|
LOGIN_COMPANY_NAME=Bulwark Webmail
|
||||||
|
|
||||||
# URL for the imprint/legal notice link on the login page
|
# URL for the imprint/legal notice link on the login page
|
||||||
# LOGIN_IMPRINT_URL=https://example.com/imprint
|
# LOGIN_IMPRINT_URL=https://example.com/imprint
|
||||||
@@ -86,7 +94,7 @@ JMAP_SERVER_URL=https://your-jmap-server.com
|
|||||||
# LOGIN_PRIVACY_POLICY_URL=https://example.com/privacy
|
# LOGIN_PRIVACY_POLICY_URL=https://example.com/privacy
|
||||||
|
|
||||||
# URL for the company website link on the login page
|
# URL for the company website link on the login page
|
||||||
# LOGIN_WEBSITE_URL=https://example.com
|
LOGIN_WEBSITE_URL=https://bulwarkmail.org
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Legacy Build-time Variables (still supported as fallback)
|
# Legacy Build-time Variables (still supported as fallback)
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ export default function LoginPage() {
|
|||||||
const t = useTranslations("login");
|
const t = useTranslations("login");
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { login, isLoading, error, clearError, isAuthenticated } = useAuthStore();
|
const { login, isLoading, error, clearError, isAuthenticated } = useAuthStore();
|
||||||
const { theme, setTheme, initializeTheme } = useThemeStore();
|
const { theme, setTheme, initializeTheme } = useThemeStore((s) => ({ theme: s.theme, setTheme: s.setTheme, initializeTheme: s.initializeTheme }));
|
||||||
const { appName, jmapServerUrl: serverUrl, oauthEnabled, oauthOnly, oauthClientId, oauthIssuerUrl, rememberMeEnabled, devMode, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError } = useConfig();
|
const { appName, jmapServerUrl: serverUrl, oauthEnabled, oauthOnly, oauthClientId, oauthIssuerUrl, rememberMeEnabled, devMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError } = useConfig();
|
||||||
|
const resolvedTheme = useThemeStore((s) => s.resolvedTheme);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
username: "",
|
username: "",
|
||||||
@@ -416,8 +417,12 @@ export default function LoginPage() {
|
|||||||
<div className="rounded-2xl border border-border/60 bg-background/80 backdrop-blur-sm shadow-xl shadow-black/5 dark:shadow-black/20 overflow-hidden">
|
<div className="rounded-2xl border border-border/60 bg-background/80 backdrop-blur-sm shadow-xl shadow-black/5 dark:shadow-black/20 overflow-hidden">
|
||||||
{/* Header section with logo */}
|
{/* Header section with logo */}
|
||||||
<div className="px-8 pt-10 pb-6 text-center">
|
<div className="px-8 pt-10 pb-6 text-center">
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary to-primary/80 mb-5 shadow-lg shadow-primary/25">
|
<div className="inline-flex items-center justify-center w-16 h-16 mb-5">
|
||||||
<Mail className="w-8 h-8 text-primary-foreground" />
|
<img
|
||||||
|
src={resolvedTheme === 'dark' ? loginLogoDarkUrl : loginLogoLightUrl}
|
||||||
|
alt={appName}
|
||||||
|
className="max-w-16 max-h-16 object-contain"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-semibold text-foreground tracking-tight">
|
<h1 className="text-2xl font-semibold text-foreground tracking-tight">
|
||||||
{appName}
|
{appName}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export async function GET() {
|
|||||||
settingsSyncEnabled: process.env.SETTINGS_SYNC_ENABLED === 'true' && !!process.env.SESSION_SECRET,
|
settingsSyncEnabled: process.env.SETTINGS_SYNC_ENABLED === 'true' && !!process.env.SESSION_SECRET,
|
||||||
stalwartFeaturesEnabled: process.env.STALWART_FEATURES !== 'false',
|
stalwartFeaturesEnabled: process.env.STALWART_FEATURES !== 'false',
|
||||||
devMode: process.env.DEV_MOCK_JMAP === 'true',
|
devMode: process.env.DEV_MOCK_JMAP === 'true',
|
||||||
|
loginLogoLightUrl: process.env.LOGIN_LOGO_LIGHT_URL || '/branding/Bulwark_Logo_Color.svg',
|
||||||
|
loginLogoDarkUrl: process.env.LOGIN_LOGO_DARK_URL || '/branding/Bulwark_Logo_White.svg',
|
||||||
loginCompanyName: process.env.LOGIN_COMPANY_NAME || '',
|
loginCompanyName: process.env.LOGIN_COMPANY_NAME || '',
|
||||||
loginImprintUrl: process.env.LOGIN_IMPRINT_URL || '',
|
loginImprintUrl: process.env.LOGIN_IMPRINT_URL || '',
|
||||||
loginPrivacyPolicyUrl: process.env.LOGIN_PRIVACY_POLICY_URL || '',
|
loginPrivacyPolicyUrl: process.env.LOGIN_PRIVACY_POLICY_URL || '',
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface ConfigData {
|
|||||||
settingsSyncEnabled: boolean;
|
settingsSyncEnabled: boolean;
|
||||||
stalwartFeaturesEnabled: boolean;
|
stalwartFeaturesEnabled: boolean;
|
||||||
devMode: boolean;
|
devMode: boolean;
|
||||||
|
loginLogoLightUrl: string;
|
||||||
|
loginLogoDarkUrl: string;
|
||||||
loginCompanyName: string;
|
loginCompanyName: string;
|
||||||
loginImprintUrl: string;
|
loginImprintUrl: string;
|
||||||
loginPrivacyPolicyUrl: string;
|
loginPrivacyPolicyUrl: string;
|
||||||
@@ -77,6 +79,8 @@ export function useConfig(): AppConfig {
|
|||||||
settingsSyncEnabled: configCache?.settingsSyncEnabled || false,
|
settingsSyncEnabled: configCache?.settingsSyncEnabled || false,
|
||||||
stalwartFeaturesEnabled: configCache?.stalwartFeaturesEnabled ?? true,
|
stalwartFeaturesEnabled: configCache?.stalwartFeaturesEnabled ?? true,
|
||||||
devMode: configCache?.devMode || false,
|
devMode: configCache?.devMode || false,
|
||||||
|
loginLogoLightUrl: configCache?.loginLogoLightUrl || '/branding/Bulwark_Logo_Color.svg',
|
||||||
|
loginLogoDarkUrl: configCache?.loginLogoDarkUrl || '/branding/Bulwark_Logo_White.svg',
|
||||||
loginCompanyName: configCache?.loginCompanyName || '',
|
loginCompanyName: configCache?.loginCompanyName || '',
|
||||||
loginImprintUrl: configCache?.loginImprintUrl || '',
|
loginImprintUrl: configCache?.loginImprintUrl || '',
|
||||||
loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '',
|
loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '',
|
||||||
@@ -99,6 +103,8 @@ export function useConfig(): AppConfig {
|
|||||||
settingsSyncEnabled: configCache.settingsSyncEnabled,
|
settingsSyncEnabled: configCache.settingsSyncEnabled,
|
||||||
stalwartFeaturesEnabled: configCache.stalwartFeaturesEnabled,
|
stalwartFeaturesEnabled: configCache.stalwartFeaturesEnabled,
|
||||||
devMode: configCache.devMode,
|
devMode: configCache.devMode,
|
||||||
|
loginLogoLightUrl: configCache.loginLogoLightUrl,
|
||||||
|
loginLogoDarkUrl: configCache.loginLogoDarkUrl,
|
||||||
loginCompanyName: configCache.loginCompanyName,
|
loginCompanyName: configCache.loginCompanyName,
|
||||||
loginImprintUrl: configCache.loginImprintUrl,
|
loginImprintUrl: configCache.loginImprintUrl,
|
||||||
loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl,
|
loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl,
|
||||||
@@ -122,6 +128,8 @@ export function useConfig(): AppConfig {
|
|||||||
settingsSyncEnabled: data.settingsSyncEnabled,
|
settingsSyncEnabled: data.settingsSyncEnabled,
|
||||||
stalwartFeaturesEnabled: data.stalwartFeaturesEnabled,
|
stalwartFeaturesEnabled: data.stalwartFeaturesEnabled,
|
||||||
devMode: data.devMode,
|
devMode: data.devMode,
|
||||||
|
loginLogoLightUrl: data.loginLogoLightUrl,
|
||||||
|
loginLogoDarkUrl: data.loginLogoDarkUrl,
|
||||||
loginCompanyName: data.loginCompanyName,
|
loginCompanyName: data.loginCompanyName,
|
||||||
loginImprintUrl: data.loginImprintUrl,
|
loginImprintUrl: data.loginImprintUrl,
|
||||||
loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl,
|
loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -65,6 +65,8 @@ CFG_SETTINGS_DATA_DIR="./data/settings"
|
|||||||
CFG_LOG_FORMAT="text"
|
CFG_LOG_FORMAT="text"
|
||||||
CFG_LOG_LEVEL="info"
|
CFG_LOG_LEVEL="info"
|
||||||
CFG_LOGIN_COMPANY_NAME=""
|
CFG_LOGIN_COMPANY_NAME=""
|
||||||
|
CFG_LOGIN_LOGO_LIGHT_URL=""
|
||||||
|
CFG_LOGIN_LOGO_DARK_URL=""
|
||||||
CFG_LOGIN_IMPRINT_URL=""
|
CFG_LOGIN_IMPRINT_URL=""
|
||||||
CFG_LOGIN_PRIVACY_POLICY_URL=""
|
CFG_LOGIN_PRIVACY_POLICY_URL=""
|
||||||
CFG_LOGIN_WEBSITE_URL=""
|
CFG_LOGIN_WEBSITE_URL=""
|
||||||
@@ -768,13 +770,19 @@ screen_login_customization() {
|
|||||||
echo -e " ${DIM}Shown on the login page footer. Example: Acme Corp${RESET}"
|
echo -e " ${DIM}Shown on the login page footer. Example: Acme Corp${RESET}"
|
||||||
prompt_value "Company / organization name" "$CFG_LOGIN_COMPANY_NAME" "CFG_LOGIN_COMPANY_NAME"
|
prompt_value "Company / organization name" "$CFG_LOGIN_COMPANY_NAME" "CFG_LOGIN_COMPANY_NAME"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo -e " ${DIM}Custom logo URLs for the login page (PNG, SVG, etc.)${RESET}"
|
||||||
|
echo -e " ${DIM}Leave blank to use the default Bulwark logo.${RESET}"
|
||||||
|
prompt_value "Logo URL (light mode)" "$CFG_LOGIN_LOGO_LIGHT_URL" "CFG_LOGIN_LOGO_LIGHT_URL"
|
||||||
|
echo ""
|
||||||
|
prompt_value "Logo URL (dark mode)" "$CFG_LOGIN_LOGO_DARK_URL" "CFG_LOGIN_LOGO_DARK_URL"
|
||||||
|
echo ""
|
||||||
prompt_value "Website URL" "$CFG_LOGIN_WEBSITE_URL" "CFG_LOGIN_WEBSITE_URL"
|
prompt_value "Website URL" "$CFG_LOGIN_WEBSITE_URL" "CFG_LOGIN_WEBSITE_URL"
|
||||||
echo ""
|
echo ""
|
||||||
prompt_value "Imprint / legal notice URL" "$CFG_LOGIN_IMPRINT_URL" "CFG_LOGIN_IMPRINT_URL"
|
prompt_value "Imprint / legal notice URL" "$CFG_LOGIN_IMPRINT_URL" "CFG_LOGIN_IMPRINT_URL"
|
||||||
echo ""
|
echo ""
|
||||||
prompt_value "Privacy policy URL" "$CFG_LOGIN_PRIVACY_POLICY_URL" "CFG_LOGIN_PRIVACY_POLICY_URL"
|
prompt_value "Privacy policy URL" "$CFG_LOGIN_PRIVACY_POLICY_URL" "CFG_LOGIN_PRIVACY_POLICY_URL"
|
||||||
|
|
||||||
if [[ -z "$CFG_LOGIN_COMPANY_NAME" && -z "$CFG_LOGIN_WEBSITE_URL" && -z "$CFG_LOGIN_IMPRINT_URL" && -z "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
|
if [[ -z "$CFG_LOGIN_COMPANY_NAME" && -z "$CFG_LOGIN_LOGO_LIGHT_URL" && -z "$CFG_LOGIN_LOGO_DARK_URL" && -z "$CFG_LOGIN_WEBSITE_URL" && -z "$CFG_LOGIN_IMPRINT_URL" && -z "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
note "No branding configured. The login page will show defaults."
|
note "No branding configured. The login page will show defaults."
|
||||||
fi
|
fi
|
||||||
@@ -884,9 +892,13 @@ screen_summary() {
|
|||||||
|
|
||||||
# Login page
|
# Login page
|
||||||
echo -e " ${CYAN}${BOLD}BRANDING${RESET}"
|
echo -e " ${CYAN}${BOLD}BRANDING${RESET}"
|
||||||
if [[ -n "$CFG_LOGIN_COMPANY_NAME" || -n "$CFG_LOGIN_WEBSITE_URL" || -n "$CFG_LOGIN_IMPRINT_URL" || -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
|
if [[ -n "$CFG_LOGIN_COMPANY_NAME" || -n "$CFG_LOGIN_LOGO_LIGHT_URL" || -n "$CFG_LOGIN_LOGO_DARK_URL" || -n "$CFG_LOGIN_WEBSITE_URL" || -n "$CFG_LOGIN_IMPRINT_URL" || -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
|
||||||
[[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && \
|
[[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && \
|
||||||
echo -e " Company Name .......... ${BOLD}${CFG_LOGIN_COMPANY_NAME}${RESET}"
|
echo -e " Company Name .......... ${BOLD}${CFG_LOGIN_COMPANY_NAME}${RESET}"
|
||||||
|
[[ -n "$CFG_LOGIN_LOGO_LIGHT_URL" ]] && \
|
||||||
|
echo -e " Logo (light mode) ..... ${BOLD}${CFG_LOGIN_LOGO_LIGHT_URL}${RESET}"
|
||||||
|
[[ -n "$CFG_LOGIN_LOGO_DARK_URL" ]] && \
|
||||||
|
echo -e " Logo (dark mode) ...... ${BOLD}${CFG_LOGIN_LOGO_DARK_URL}${RESET}"
|
||||||
[[ -n "$CFG_LOGIN_WEBSITE_URL" ]] && \
|
[[ -n "$CFG_LOGIN_WEBSITE_URL" ]] && \
|
||||||
echo -e " Website URL ........... ${BOLD}${CFG_LOGIN_WEBSITE_URL}${RESET}"
|
echo -e " Website URL ........... ${BOLD}${CFG_LOGIN_WEBSITE_URL}${RESET}"
|
||||||
[[ -n "$CFG_LOGIN_IMPRINT_URL" ]] && \
|
[[ -n "$CFG_LOGIN_IMPRINT_URL" ]] && \
|
||||||
@@ -995,6 +1007,8 @@ LOG_LEVEL=${CFG_LOG_LEVEL}
|
|||||||
ENVEOF
|
ENVEOF
|
||||||
|
|
||||||
[[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && echo "LOGIN_COMPANY_NAME=${CFG_LOGIN_COMPANY_NAME}" >> "$ENV_FILE"
|
[[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && echo "LOGIN_COMPANY_NAME=${CFG_LOGIN_COMPANY_NAME}" >> "$ENV_FILE"
|
||||||
|
[[ -n "$CFG_LOGIN_LOGO_LIGHT_URL" ]] && echo "LOGIN_LOGO_LIGHT_URL=${CFG_LOGIN_LOGO_LIGHT_URL}" >> "$ENV_FILE"
|
||||||
|
[[ -n "$CFG_LOGIN_LOGO_DARK_URL" ]] && echo "LOGIN_LOGO_DARK_URL=${CFG_LOGIN_LOGO_DARK_URL}" >> "$ENV_FILE"
|
||||||
[[ -n "$CFG_LOGIN_IMPRINT_URL" ]] && echo "LOGIN_IMPRINT_URL=${CFG_LOGIN_IMPRINT_URL}" >> "$ENV_FILE"
|
[[ -n "$CFG_LOGIN_IMPRINT_URL" ]] && echo "LOGIN_IMPRINT_URL=${CFG_LOGIN_IMPRINT_URL}" >> "$ENV_FILE"
|
||||||
[[ -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]] && echo "LOGIN_PRIVACY_POLICY_URL=${CFG_LOGIN_PRIVACY_POLICY_URL}" >> "$ENV_FILE"
|
[[ -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]] && echo "LOGIN_PRIVACY_POLICY_URL=${CFG_LOGIN_PRIVACY_POLICY_URL}" >> "$ENV_FILE"
|
||||||
[[ -n "$CFG_LOGIN_WEBSITE_URL" ]] && echo "LOGIN_WEBSITE_URL=${CFG_LOGIN_WEBSITE_URL}" >> "$ENV_FILE"
|
[[ -n "$CFG_LOGIN_WEBSITE_URL" ]] && echo "LOGIN_WEBSITE_URL=${CFG_LOGIN_WEBSITE_URL}" >> "$ENV_FILE"
|
||||||
@@ -1204,6 +1218,8 @@ load_existing_config() {
|
|||||||
get_env_val "LOG_FORMAT"; [[ -n "$val" ]] && CFG_LOG_FORMAT="$val"
|
get_env_val "LOG_FORMAT"; [[ -n "$val" ]] && CFG_LOG_FORMAT="$val"
|
||||||
get_env_val "LOG_LEVEL"; [[ -n "$val" ]] && CFG_LOG_LEVEL="$val"
|
get_env_val "LOG_LEVEL"; [[ -n "$val" ]] && CFG_LOG_LEVEL="$val"
|
||||||
get_env_val "LOGIN_COMPANY_NAME"; [[ -n "$val" ]] && CFG_LOGIN_COMPANY_NAME="$val"
|
get_env_val "LOGIN_COMPANY_NAME"; [[ -n "$val" ]] && CFG_LOGIN_COMPANY_NAME="$val"
|
||||||
|
get_env_val "LOGIN_LOGO_LIGHT_URL"; [[ -n "$val" ]] && CFG_LOGIN_LOGO_LIGHT_URL="$val"
|
||||||
|
get_env_val "LOGIN_LOGO_DARK_URL"; [[ -n "$val" ]] && CFG_LOGIN_LOGO_DARK_URL="$val"
|
||||||
get_env_val "LOGIN_IMPRINT_URL"; [[ -n "$val" ]] && CFG_LOGIN_IMPRINT_URL="$val"
|
get_env_val "LOGIN_IMPRINT_URL"; [[ -n "$val" ]] && CFG_LOGIN_IMPRINT_URL="$val"
|
||||||
get_env_val "LOGIN_PRIVACY_POLICY_URL"; [[ -n "$val" ]] && CFG_LOGIN_PRIVACY_POLICY_URL="$val"
|
get_env_val "LOGIN_PRIVACY_POLICY_URL"; [[ -n "$val" ]] && CFG_LOGIN_PRIVACY_POLICY_URL="$val"
|
||||||
get_env_val "LOGIN_WEBSITE_URL"; [[ -n "$val" ]] && CFG_LOGIN_WEBSITE_URL="$val"
|
get_env_val "LOGIN_WEBSITE_URL"; [[ -n "$val" ]] && CFG_LOGIN_WEBSITE_URL="$val"
|
||||||
|
|||||||