Files
webmail/lib/admin/types.ts
T

249 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Admin dashboard types
/**
* Operator-authored admin record. Lives in admin.json under the config dir
* and can be mounted read-only after setup. Only the password hash itself
* is config; mutable timestamps live in AdminStateData.
*/
export interface AdminConfigData {
passwordHash: string | undefined;
passwordHashFile: string | undefined;
}
/**
* Runtime-mutable admin record. Lives in admin-state.json under the state
* dir. Updated on every login and password change, so it must stay writable.
*/
export interface AdminStateData {
createdAt: string;
lastLogin: string | null;
passwordChangedAt: string;
}
/**
* Combined view used by getAdminMeta() and tests. Constructed by merging
* admin.json + admin-state.json at read time.
*/
export interface AdminData extends AdminConfigData, AdminStateData {}
export interface AdminSessionPayload {
role: 'admin';
iat: number;
exp: number;
}
export interface SettingRestriction {
locked?: boolean;
value?: unknown;
hidden?: boolean;
allowedValues?: unknown[];
min?: number;
max?: number;
}
export interface FeatureGates {
pluginsEnabled: boolean;
pluginsUploadEnabled: boolean;
requirePluginApproval: boolean;
themesEnabled: boolean;
sidebarAppsEnabled: boolean;
userThemesEnabled: boolean;
settingsExportEnabled: boolean;
customKeywordsEnabled: boolean;
templatesEnabled: boolean;
calendarEnabled: boolean;
calendarTasksEnabled: boolean;
smimeEnabled: boolean;
externalContentEnabled: boolean;
debugModeEnabled: boolean;
folderIconsEnabled: boolean;
hoverActionsConfigEnabled: boolean;
filesEnabled: boolean;
contactsEnabled: boolean;
/** @deprecated Folded into `crossAllViewEnabled`; normalized forward on policy load. */
allMailViewEnabled: boolean;
crossUnreadViewEnabled: boolean;
crossStarredViewEnabled: boolean;
crossAllViewEnabled: boolean;
unifiedCrossAccountEnabled: boolean;
}
export const DEFAULT_FEATURE_GATES: FeatureGates = {
pluginsEnabled: false,
pluginsUploadEnabled: true,
requirePluginApproval: true,
themesEnabled: true,
sidebarAppsEnabled: true,
userThemesEnabled: true,
settingsExportEnabled: true,
customKeywordsEnabled: true,
templatesEnabled: true,
calendarEnabled: true,
calendarTasksEnabled: true,
smimeEnabled: true,
externalContentEnabled: true,
debugModeEnabled: true,
folderIconsEnabled: true,
hoverActionsConfigEnabled: true,
filesEnabled: true,
contactsEnabled: true,
allMailViewEnabled: false,
crossUnreadViewEnabled: false,
crossStarredViewEnabled: false,
crossAllViewEnabled: false,
unifiedCrossAccountEnabled: false,
};
export interface ThemePolicy {
/** Built-in theme IDs that are disabled (hidden from users) */
disabledBuiltinThemes: string[];
/** Admin-deployed theme IDs that are disabled (hidden from users) */
disabledThemes: string[];
/** Default theme ID for new users (null = system default) */
defaultThemeId: string | null;
}
export const DEFAULT_THEME_POLICY: ThemePolicy = {
disabledBuiltinThemes: [],
disabledThemes: [],
defaultThemeId: null,
};
/** One entry of the Web Push relay list users can pick from. */
export interface PushRelayOption {
/** Name shown in the relay picker. Empty falls back to the URL host. */
label: string;
/** Relay base URL, without a trailing slash. */
url: string;
}
export interface SettingsPolicy {
restrictions: Record<string, SettingRestriction>;
features: FeatureGates;
defaults: Record<string, unknown>;
themePolicy: ThemePolicy;
/** Plugin IDs that are force-enabled (users cannot disable) */
forceEnabledPlugins: string[];
/** Plugin IDs that have been approved by admin (users can enable) */
approvedPlugins: string[];
/** Theme IDs that are force-enabled (users cannot deactivate) */
forceEnabledThemes: string[];
/**
* Extra Web Push relays offered alongside the built-in default. Users pick
* from this list; only admins can introduce a relay URL.
*/
pushRelays?: PushRelayOption[];
/** Relay preselected for users. Empty means the built-in default. */
pushRelayUrl?: string;
/** When true, users are pinned to pushRelayUrl and cannot pick another relay. */
pushRelayUrlLocked?: boolean;
}
export const DEFAULT_POLICY: SettingsPolicy = {
restrictions: {},
features: { ...DEFAULT_FEATURE_GATES },
defaults: {},
themePolicy: { ...DEFAULT_THEME_POLICY },
forceEnabledPlugins: [],
approvedPlugins: [],
forceEnabledThemes: [],
pushRelays: [],
pushRelayUrl: '',
pushRelayUrlLocked: false,
};
export interface AuditEntry {
ts: string;
action: string;
detail: Record<string, unknown>;
ip: string;
}
/** Config keys that map to environment variables */
export const CONFIG_ENV_MAP: Record<string, { envVar: string; fileEnvVar?: string; type: 'string' | 'boolean' | 'url' | 'enum' | 'json'; defaultValue: unknown; enumValues?: string[] }> = {
appName: { envVar: 'APP_NAME', type: 'string', defaultValue: 'Webmail' },
appShortName: { envVar: 'APP_SHORT_NAME', type: 'string', defaultValue: '' },
appDescription: { envVar: 'APP_DESCRIPTION', type: 'string', defaultValue: '' },
searchEngineIndexing: { envVar: 'SEARCH_ENGINE_INDEXING', type: 'boolean', defaultValue: false },
jmapServerUrl: { envVar: 'JMAP_SERVER_URL', type: 'url', defaultValue: '' },
stalwartFeaturesEnabled: { envVar: 'STALWART_FEATURES', type: 'boolean', defaultValue: true },
demoMode: { envVar: 'DEMO_MODE', type: 'boolean', defaultValue: false },
devMode: { envVar: 'DEV_MOCK_JMAP', type: 'boolean', defaultValue: false },
faviconUrl: { envVar: 'FAVICON_URL', type: 'url', defaultValue: '/branding/Bulwark_Favicon.svg' },
pwaIconUrl: { envVar: 'PWA_ICON_URL', type: 'url', defaultValue: '' },
pwaScreenshotMobileUrl: { envVar: 'PWA_SCREENSHOT_MOBILE_URL', type: 'url', defaultValue: '' },
pwaScreenshotDesktopUrl: { envVar: 'PWA_SCREENSHOT_DESKTOP_URL', type: 'url', defaultValue: '' },
pwaThemeColor: { envVar: 'PWA_THEME_COLOR', type: 'string', defaultValue: '#ffffff' },
pwaBackgroundColor: { envVar: 'PWA_BACKGROUND_COLOR', type: 'string', defaultValue: '#ffffff' },
appLogoLightUrl: { envVar: 'APP_LOGO_LIGHT_URL', type: 'url', defaultValue: '' },
appLogoDarkUrl: { envVar: 'APP_LOGO_DARK_URL', type: 'url', defaultValue: '' },
loginLogoLightUrl: { envVar: 'LOGIN_LOGO_LIGHT_URL', type: 'url', defaultValue: '/branding/Bulwark_Logo_Color.svg' },
loginLogoDarkUrl: { envVar: 'LOGIN_LOGO_DARK_URL', type: 'url', defaultValue: '/branding/Bulwark_Logo_White.svg' },
loginCompanyName: { envVar: 'LOGIN_COMPANY_NAME', type: 'string', defaultValue: '' },
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: '' },
// 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
// heading ({appName}) and subtitle can be hidden when the logo already
// reads as the brand (e.g. a wordmark) and they'd be redundant.
loginLogoMaxHeight: { envVar: 'LOGIN_LOGO_MAX_HEIGHT', type: 'string', defaultValue: '' },
loginLogoMaxWidth: { envVar: 'LOGIN_LOGO_MAX_WIDTH', type: 'string', defaultValue: '' },
loginShowHeading: { envVar: 'LOGIN_SHOW_HEADING', type: 'boolean', defaultValue: true },
loginShowSubtitle: { envVar: 'LOGIN_SHOW_SUBTITLE', type: 'boolean', defaultValue: true },
// Hide the manual "I have a 2FA code" toggle on the login form. Deployments
// that delegate auth to an external directory (LDAP/OIDC) where 2FA lives in
// the IdP have no server-side TOTP, so the toggle only leads to a failed
// login. Server-required TOTP (totp_required) still shows regardless.
loginShowTotp: { envVar: 'LOGIN_SHOW_TOTP', type: 'boolean', defaultValue: true },
// Show the build version in the login footer. Off keeps the exact version
// from being disclosed to unauthenticated visitors.
loginShowVersion: { envVar: 'LOGIN_SHOW_VERSION', type: 'boolean', defaultValue: true },
oauthEnabled: { envVar: 'OAUTH_ENABLED', type: 'boolean', defaultValue: false },
oauthOnly: { envVar: 'OAUTH_ONLY', type: 'boolean', defaultValue: false },
oauthClientId: { envVar: 'OAUTH_CLIENT_ID', type: 'string', defaultValue: '' },
oauthClientSecret: { envVar: 'OAUTH_CLIENT_SECRET', fileEnvVar: 'OAUTH_CLIENT_SECRET_FILE', type: 'string', defaultValue: '' },
oauthIssuerUrl: { envVar: 'OAUTH_ISSUER_URL', type: 'url', defaultValue: '' },
// Overrides only the user-facing authorize endpoint. Discovery, token exchange
// and refresh continue to use the canonical OAUTH_ISSUER_URL. Lets a per-brand
// authorize host front a single canonical issuer. Empty = use the discovered
// authorization_endpoint.
oauthAuthorizeUrl: { envVar: 'OAUTH_AUTHORIZE_URL', type: 'url', defaultValue: '' },
oauthScopes: { envVar: 'OAUTH_SCOPES', type: 'string', defaultValue: '' },
oauthExtraScopes: { envVar: 'OAUTH_EXTRA_SCOPES', type: 'string', defaultValue: '' },
oauthAllowPrivateEndpoints: { envVar: 'OAUTH_ALLOW_PRIVATE_ENDPOINTS', type: 'boolean', defaultValue: false },
allowCustomJmapEndpoint: { envVar: 'ALLOW_CUSTOM_JMAP_ENDPOINT', type: 'boolean', defaultValue: false },
// What being a Stalwart admin grants inside the Bulwark admin dashboard (#870).
// auto - Stalwart admins see the shield and are signed into /admin
// without the Bulwark admin password (legacy behaviour).
// password - Stalwart admins see the shield, but must enter the Bulwark
// admin password like everyone else.
// off - Stalwart admin status is ignored; /admin is reachable only
// via /admin/login with the Bulwark admin password.
// "password" and "off" require an admin password to be configured, or the
// dashboard would become unreachable.
stalwartAdminAccess: { envVar: 'STALWART_ADMIN_ACCESS', type: 'enum', defaultValue: 'auto', enumValues: ['auto', 'password', 'off'] },
jmapServers: { envVar: 'JMAP_SERVERS', type: 'json', defaultValue: [] },
jmapServerAutoPickByDomain: { envVar: 'JMAP_SERVER_AUTO_PICK_BY_DOMAIN', type: 'boolean', defaultValue: false },
domainBranding: { envVar: 'DOMAIN_BRANDING', type: 'json', defaultValue: [] },
autoSsoEnabled: { envVar: 'AUTO_SSO_ENABLED', type: 'boolean', defaultValue: false },
cookieSameSite: { envVar: 'COOKIE_SAME_SITE', type: 'enum', defaultValue: 'lax', enumValues: ['lax', 'strict', 'none'] },
allowedFrameAncestors: { envVar: 'ALLOWED_FRAME_ANCESTORS', type: 'string', defaultValue: '' },
parentOrigin: { envVar: 'NEXT_PUBLIC_PARENT_ORIGIN', type: 'string', defaultValue: '' },
settingsSyncEnabled: { envVar: 'SETTINGS_SYNC_ENABLED', type: 'boolean', defaultValue: false },
logFormat: { envVar: 'LOG_FORMAT', type: 'enum', defaultValue: 'text', enumValues: ['text', 'json'] },
logLevel: { envVar: 'LOG_LEVEL', type: 'enum', defaultValue: 'info', enumValues: ['error', 'warn', 'info', 'debug'] },
sessionSecret: { envVar: 'SESSION_SECRET', fileEnvVar: 'SESSION_SECRET_FILE', type: 'string', defaultValue: '' },
extensionDirectoryUrl: { envVar: 'EXTENSION_DIRECTORY_URL', type: 'url', defaultValue: 'https://extensions.bulwarkmail.org' },
};
/** Keys that should never be exposed to the client config endpoint */
export const SENSITIVE_CONFIG_KEYS = new Set(['oauthClientSecret', 'sessionSecret']);
/** Admin session cookie name */
export const ADMIN_SESSION_COOKIE = 'admin_session';
/** Default admin session TTL in seconds */
export const DEFAULT_ADMIN_SESSION_TTL = 3600;