mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: link the running build's source from the About card #1161
This commit is contained in:
@@ -302,6 +302,12 @@ LOGIN_COMPANY_NAME=Bulwark Webmail
|
||||
# URL for the company website link on the login page.
|
||||
LOGIN_WEBSITE_URL=https://bulwarkmail.org
|
||||
|
||||
# Where users get the source code of this build, linked from Settings >
|
||||
# About. Bulwark is AGPL-3.0-only: if you run a modified build, set this to
|
||||
# the source of exactly what you deploy (your fork, at the deployed commit).
|
||||
# Unset links to the upstream repository. Only http(s) URLs are used.
|
||||
# SOURCE_CODE_URL=https://git.example.com/acme/webmail/tree/v1.2.3
|
||||
|
||||
# Cap the login logo's rendered size. Any CSS length ("120px", "8rem").
|
||||
# Unset means the logo renders at its natural size.
|
||||
# LOGIN_LOGO_MAX_HEIGHT=96px
|
||||
|
||||
@@ -4,6 +4,7 @@ import { configManager } from '@/lib/admin/config-manager';
|
||||
import { parseJmapServers, redactJmapServers } from '@/lib/admin/jmap-servers';
|
||||
import { hasSessionSecret } from '@/lib/auth/session-secret';
|
||||
import { getOauthScopes } from '@/lib/oauth/tokens';
|
||||
import { httpUrlOrEmpty } from '@/lib/config-validation';
|
||||
import {
|
||||
matchDomainBranding,
|
||||
parseDomainBranding,
|
||||
@@ -95,6 +96,7 @@ export async function GET(request: NextRequest) {
|
||||
autoSsoEnabled: configManager.get<boolean>('autoSsoEnabled', false),
|
||||
embeddedMode: !!allowedFrameAncestors && allowedFrameAncestors !== "'none'",
|
||||
parentOrigin: configManager.get<string>('parentOrigin', ''),
|
||||
sourceCodeUrl: httpUrlOrEmpty(configManager.get<string>('sourceCodeUrl', '')),
|
||||
},
|
||||
{
|
||||
// Runtime settings can change without new asset URLs. Prevent stale
|
||||
|
||||
@@ -16,6 +16,7 @@ import { SpamSiegeGame } from './spam-siege-game';
|
||||
|
||||
const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
|
||||
const GIT_COMMIT = process.env.NEXT_PUBLIC_GIT_COMMIT || "unknown";
|
||||
const UPSTREAM_SOURCE_URL = "https://github.com/bulwarkmail/webmail";
|
||||
|
||||
function VersionUpdateTag() {
|
||||
const status = useUpdateStore((s) => s.status);
|
||||
@@ -54,7 +55,9 @@ export function AboutDataSettings() {
|
||||
const tSettings = useTranslations('settings');
|
||||
const { settingsSyncDisabled, updateSetting, resetToDefaults, exportSettings, importSettings } =
|
||||
useSettingsStore();
|
||||
const { settingsSyncEnabled } = useConfig();
|
||||
// A modified build must offer its own source (AGPL-3.0 §13), so a
|
||||
// configured URL replaces the upstream repository link.
|
||||
const { settingsSyncEnabled, sourceCodeUrl } = useConfig();
|
||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||
const [showRefreshConfirm, setShowRefreshConfirm] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -157,12 +160,12 @@ export function AboutDataSettings() {
|
||||
</div>
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com/bulwarkmail/webmail"
|
||||
href={sourceCodeUrl || UPSTREAM_SOURCE_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
GitHub <ExternalLink className="w-3 h-3" />
|
||||
{sourceCodeUrl ? t('about.source_code') : 'GitHub'} <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface ConfigData {
|
||||
jmapServerAutoPickByDomain: boolean;
|
||||
embeddedMode: boolean;
|
||||
parentOrigin: string;
|
||||
sourceCodeUrl: string;
|
||||
}
|
||||
|
||||
interface AppConfig extends ConfigData {
|
||||
@@ -183,6 +184,7 @@ export function useConfig(): AppConfig {
|
||||
jmapServerAutoPickByDomain: configCache?.jmapServerAutoPickByDomain || false,
|
||||
embeddedMode: configCache?.embeddedMode || false,
|
||||
parentOrigin: configCache?.parentOrigin || '',
|
||||
sourceCodeUrl: configCache?.sourceCodeUrl || '',
|
||||
isLoading: !configCache,
|
||||
error: null,
|
||||
});
|
||||
@@ -226,6 +228,7 @@ export function useConfig(): AppConfig {
|
||||
jmapServerAutoPickByDomain: configCache.jmapServerAutoPickByDomain || false,
|
||||
embeddedMode: configCache.embeddedMode,
|
||||
parentOrigin: configCache.parentOrigin,
|
||||
sourceCodeUrl: configCache.sourceCodeUrl,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -270,6 +273,7 @@ export function useConfig(): AppConfig {
|
||||
jmapServerAutoPickByDomain: data.jmapServerAutoPickByDomain || false,
|
||||
embeddedMode: data.embeddedMode,
|
||||
parentOrigin: data.parentOrigin,
|
||||
sourceCodeUrl: data.sourceCodeUrl,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ const MANAGED_ENV = [
|
||||
'SESSION_SECRET_FILE', 'SETTINGS_SYNC_ENABLED', 'STALWART_FEATURES', 'DEV_MOCK_JMAP',
|
||||
'FAVICON_URL', 'APP_LOGO_LIGHT_URL', 'APP_LOGO_DARK_URL', 'LOGIN_COMPANY_NAME',
|
||||
'LOGIN_IMPRINT_URL', 'LOGIN_PRIVACY_POLICY_URL', 'LOGIN_WEBSITE_URL', 'DOMAIN_BRANDING',
|
||||
'JMAP_SERVERS', 'JMAP_SERVER_AUTO_PICK_BY_DOMAIN',
|
||||
'JMAP_SERVERS', 'JMAP_SERVER_AUTO_PICK_BY_DOMAIN', 'SOURCE_CODE_URL',
|
||||
] as const;
|
||||
|
||||
describe('config API route', () => {
|
||||
@@ -107,6 +107,7 @@ describe('config API route', () => {
|
||||
expect(config.faviconUrl).toBe('/branding/Bulwark_Favicon.svg');
|
||||
expect(config.appLogoLightUrl).toBe('');
|
||||
expect(config.appLogoDarkUrl).toBe('');
|
||||
expect(config.sourceCodeUrl).toBe('');
|
||||
});
|
||||
|
||||
// The domain lists name every organisation served by this instance.
|
||||
@@ -161,6 +162,21 @@ describe('config API route', () => {
|
||||
expect(config.loginWebsiteUrl).toBe('https://acme.com');
|
||||
});
|
||||
|
||||
it('publishes the source code URL for the About card (#1161)', async () => {
|
||||
vi.stubEnv('SOURCE_CODE_URL', 'https://git.example.com/acme/webmail/tree/v1.2.3');
|
||||
|
||||
expect((await getConfig()).sourceCodeUrl).toBe('https://git.example.com/acme/webmail/tree/v1.2.3');
|
||||
});
|
||||
|
||||
it.each(['javascript:alert(1)', 'data:text/html,x', '/relative/path', 'not a url'])(
|
||||
'drops a source code URL that is not absolute http(s): %s',
|
||||
async (value) => {
|
||||
vi.stubEnv('SOURCE_CODE_URL', value);
|
||||
|
||||
expect((await getConfig()).sourceCodeUrl).toBe('');
|
||||
},
|
||||
);
|
||||
|
||||
it('should handle partial login customization', async () => {
|
||||
vi.stubEnv('LOGIN_COMPANY_NAME', 'Partial Corp');
|
||||
// Leave URLs unset
|
||||
|
||||
@@ -38,6 +38,7 @@ export function createConfig(overrides: Partial<ConfigData> = {}): ConfigData {
|
||||
jmapServerAutoPickByDomain: false,
|
||||
embeddedMode: false,
|
||||
parentOrigin: '',
|
||||
sourceCodeUrl: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,6 +63,13 @@ describe('applyLiteConfig (config.json for the static build)', () => {
|
||||
expect(applyLiteConfig('garbage').appName).toBe('Webmail');
|
||||
});
|
||||
|
||||
it('keeps an http(s) source code URL and drops any other scheme', () => {
|
||||
expect(applyLiteConfig({}).sourceCodeUrl).toBe('');
|
||||
expect(applyLiteConfig({ sourceCodeUrl: 'https://git.example.com/acme/webmail' }).sourceCodeUrl)
|
||||
.toBe('https://git.example.com/acme/webmail');
|
||||
expect(applyLiteConfig({ sourceCodeUrl: 'javascript:alert(1)' }).sourceCodeUrl).toBe('');
|
||||
});
|
||||
|
||||
it('keeps deployer-facing branding and login toggles', () => {
|
||||
const config = applyLiteConfig({
|
||||
appName: 'Acme Mail',
|
||||
|
||||
@@ -215,6 +215,10 @@ export const CONFIG_ENV_MAP: Record<string, { envVar: string; fileEnvVar?: strin
|
||||
loginImprintUrl: { envVar: 'LOGIN_IMPRINT_URL', type: 'url', defaultValue: '' },
|
||||
loginPrivacyPolicyUrl: { envVar: 'LOGIN_PRIVACY_POLICY_URL', type: 'url', defaultValue: '' },
|
||||
loginWebsiteUrl: { envVar: 'LOGIN_WEBSITE_URL', type: 'url', defaultValue: '' },
|
||||
// Where users get the source of the build they are running (AGPL-3.0 §13).
|
||||
// Operators of a modified build point it at their fork; the About card then
|
||||
// links there instead of the upstream repository (#1161). Empty = upstream.
|
||||
sourceCodeUrl: { envVar: 'SOURCE_CODE_URL', type: 'url', defaultValue: '' },
|
||||
// Login header customization. The logo box is otherwise a fixed 64×64
|
||||
// (w-16/h-16), which fits a wide wordmark to ~13px tall; set a max height
|
||||
// and/or width (any CSS length, e.g. "230px" or "3rem") to size it. The
|
||||
|
||||
@@ -62,8 +62,24 @@ const configValidators = {
|
||||
jmapServerAutoPickByDomain: isBoolean,
|
||||
embeddedMode: isBoolean,
|
||||
parentOrigin: isString,
|
||||
sourceCodeUrl: isString,
|
||||
} satisfies Validators<ConfigData>;
|
||||
|
||||
/**
|
||||
* An absolute http(s) URL as given, or '' for anything else, so a configured
|
||||
* link can never carry a `javascript:` or other script-running scheme.
|
||||
*/
|
||||
export function httpUrlOrEmpty(value: unknown): string {
|
||||
if (typeof value !== 'string') return '';
|
||||
const trimmed = value.trim();
|
||||
try {
|
||||
const { protocol } = new URL(trimmed);
|
||||
return protocol === 'http:' || protocol === 'https:' ? trimmed : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate the server's complete response without rewriting configured values. */
|
||||
export function isConfigData(value: unknown): value is ConfigData {
|
||||
return isRecord(value)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ConfigData } from '@/hooks/use-config';
|
||||
import type { SettingsPolicy } from '@/lib/admin/types';
|
||||
import { DEFAULT_POLICY } from '@/lib/admin/types';
|
||||
import { httpUrlOrEmpty } from '@/lib/config-validation';
|
||||
|
||||
/**
|
||||
* Runtime configuration for the static Lite build.
|
||||
@@ -40,6 +41,7 @@ export const LITE_CONFIG_KEYS = [
|
||||
'loginShowVersion',
|
||||
'embeddedMode',
|
||||
'parentOrigin',
|
||||
'sourceCodeUrl',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -129,6 +131,7 @@ export function applyLiteConfig(raw: unknown, defaults?: { jmapServerUrl?: strin
|
||||
jmapServerAutoPickByDomain: bool(input.jmapServerAutoPickByDomain, false),
|
||||
embeddedMode: bool(input.embeddedMode, false),
|
||||
parentOrigin: str(input.parentOrigin, ''),
|
||||
sourceCodeUrl: httpUrlOrEmpty(input.sourceCodeUrl),
|
||||
...LITE_FORCED_FLAGS,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "استيراد"
|
||||
},
|
||||
"about": {
|
||||
"title": "بريد Bulwark الإلكتروني"
|
||||
"title": "بريد Bulwark الإلكتروني",
|
||||
"source_code": "الشيفرة المصدرية"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "Importa"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Codi font"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Importovat"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Zdrojový kód"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1976,7 +1976,8 @@
|
||||
"button": "Importér"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Kildekode"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Importieren"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Quellcode"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "Import"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Source code"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Importar"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Código fuente"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "وارد کردن"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "کد منبع"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Importer"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Code source"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1935,7 +1935,8 @@
|
||||
"button": "ייבוא"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "קוד מקור"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1976,7 +1976,8 @@
|
||||
"button": "Importálás"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Forráskód"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Importa"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Codice sorgente"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "インポート"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "ソースコード"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "가져오기"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail 정보"
|
||||
"title": "Bulwark Webmail 정보",
|
||||
"source_code": "소스 코드"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"contacts_description": "Kontaktu sinhronizācija, adrešu grāmatu darbības un uzticamie sūtītāji"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Pirmkods"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "Импорт"
|
||||
},
|
||||
"about": {
|
||||
"title": "Тухай"
|
||||
"title": "Тухай",
|
||||
"source_code": "Эх код"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "Importer"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark nettpost"
|
||||
"title": "Bulwark nettpost",
|
||||
"source_code": "Kildekode"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Importeren"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Broncode"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Importuj"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Kod źródłowy"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Importar"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Código-fonte"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "Import"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Cod sursă"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Импортировать"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Исходный код"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "Importovať"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Zdrojový kód"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "İçe Aktar"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark Webmail"
|
||||
"title": "Bulwark Webmail",
|
||||
"source_code": "Kaynak kodu"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "Імпорт"
|
||||
},
|
||||
"about": {
|
||||
"title": "Оплот веб-пошти"
|
||||
"title": "Оплот веб-пошти",
|
||||
"source_code": "Вихідний код"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
"button": "匯入"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark 網頁郵件"
|
||||
"title": "Bulwark 網頁郵件",
|
||||
"source_code": "原始碼"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
@@ -1973,7 +1973,8 @@
|
||||
"button": "导入"
|
||||
},
|
||||
"about": {
|
||||
"title": "Bulwark 网页邮箱"
|
||||
"title": "Bulwark 网页邮箱",
|
||||
"source_code": "源代码"
|
||||
},
|
||||
"spam_siege": {
|
||||
"title": "Spam Siege",
|
||||
|
||||
Reference in New Issue
Block a user