mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 08:15:38 +00:00
A second switch under System > Usage stats, "Also send anonymous error counts", or `BULWARK_TELEMETRY_ERRORS=on|off`. Off by default, off whenever the heartbeat is off or no third-party traffic is on, and switching the heartbeat off switches it off too. With it on the heartbeat becomes schema 2: the v1 fields unchanged plus `errors`, up to 50 fingerprints of the last complete UTC day with the error name, the top frames in Bulwark's bundles (path, line, column, debug ID), the version, a bucketed count, on how many of the last 7 days it occurred, and a message only when it is a fixed error code. Without the consent the heartbeat stays exactly schema 1. The admin preview shows the v2 payload as soon as the switch is flipped.
314 lines
13 KiB
TypeScript
314 lines
13 KiB
TypeScript
/**
|
|
* The admin panel's pages and their URLs.
|
|
*
|
|
* Every page lives at `/admin/<id>`; plugins, themes and the marketplace
|
|
* have a detail page below it (`/admin/plugins/<pluginId>`). The whole tree
|
|
* is one Next route (app/(main)/admin/[[...segments]]) and switching pages
|
|
* goes through history.pushState, so the sidebar never starts a route
|
|
* transition - the same way the user settings keep their permalinks (#733).
|
|
*
|
|
* This module is plain data: the connector registry imports it on the
|
|
* server, so no React in here. Icons live in the shell.
|
|
*/
|
|
|
|
export const ADMIN_PAGE_IDS = [
|
|
'dashboard',
|
|
'settings',
|
|
'auth',
|
|
'branding',
|
|
'policy',
|
|
'organisation',
|
|
'org-signatures',
|
|
'org-templates',
|
|
'org-security',
|
|
'org-sharing',
|
|
'plugins',
|
|
'themes',
|
|
'marketplace',
|
|
'version',
|
|
'telemetry',
|
|
'monitoring',
|
|
'logs',
|
|
] as const;
|
|
|
|
export type AdminPageId = typeof ADMIN_PAGE_IDS[number];
|
|
|
|
export function isAdminPageId(value: string | null | undefined): value is AdminPageId {
|
|
return typeof value === 'string' && (ADMIN_PAGE_IDS as readonly string[]).includes(value);
|
|
}
|
|
|
|
/** `top` has no heading in the sidebar; the others do. */
|
|
export type AdminPageGroup = 'top' | 'organisation' | 'extensions' | 'system';
|
|
|
|
export interface AdminSectionDef {
|
|
/** The `id` the page gives its SettingsSection, for jump links and search. */
|
|
id: string;
|
|
title: string;
|
|
/** Extra words the sidebar search matches, besides the title. */
|
|
keywords?: string;
|
|
}
|
|
|
|
export interface AdminPageDef {
|
|
id: AdminPageId;
|
|
label: string;
|
|
/** One line under the page title. */
|
|
description: string;
|
|
group: AdminPageGroup;
|
|
/** Words the sidebar search matches, besides the label and the sections. */
|
|
keywords: string;
|
|
sections: readonly AdminSectionDef[];
|
|
/** Whether `/admin/<id>/<detail>` opens a detail page. */
|
|
hasDetail?: boolean;
|
|
}
|
|
|
|
export const ADMIN_PAGES: readonly AdminPageDef[] = [
|
|
{
|
|
id: 'dashboard',
|
|
label: 'Overview',
|
|
description: 'What this Bulwark is running and what needs attention',
|
|
group: 'top',
|
|
keywords: 'dashboard home status warnings accounts activity recent',
|
|
sections: [],
|
|
},
|
|
{
|
|
id: 'settings',
|
|
label: 'Server',
|
|
description: 'How this Bulwark connects and what the login page offers',
|
|
group: 'top',
|
|
keywords: 'settings application name jmap server url demo mode stalwart features search engine indexing',
|
|
sections: [
|
|
{ id: 'general', title: 'General', keywords: 'app name jmap url custom endpoint demo mode stalwart indexing' },
|
|
{ id: 'login', title: 'Login page', keywords: 'version 2fa totp code access token remember me heading subtitle' },
|
|
{ id: 'servers', title: 'JMAP servers', keywords: 'multi server domain auto pick list' },
|
|
{ id: 'wopi', title: 'Document editing', keywords: 'wopi collabora onlyoffice office files host url' },
|
|
{ id: 'bimi', title: 'Brand logos', keywords: 'bimi vmc cmc mark certificate sender logo icon dmarc' },
|
|
{ id: 'logging', title: 'Logging', keywords: 'log level format json text debug' },
|
|
{ id: 'sync', title: 'Settings sync', keywords: 'session secret devices' },
|
|
],
|
|
},
|
|
{
|
|
id: 'auth',
|
|
label: 'Authentication',
|
|
description: 'OAuth, single sign-on, admin access and session security',
|
|
group: 'top',
|
|
keywords: 'login password oidc openid',
|
|
sections: [
|
|
{ id: 'oauth', title: 'OAuth / OpenID Connect', keywords: 'client id secret issuer scopes private endpoints auto configure stalwart' },
|
|
{ id: 'sso', title: 'Single sign-on', keywords: 'auto sso redirect end session logout provider post-logout' },
|
|
{ id: 'admin-access', title: 'Admin access', keywords: 'stalwart admin dashboard password automatic' },
|
|
{ id: 'session', title: 'Session and security', keywords: 'cookie samesite frame ancestors parent origin embed' },
|
|
],
|
|
},
|
|
{
|
|
id: 'branding',
|
|
label: 'Branding',
|
|
description: 'Logos, colours and company details, per domain if needed',
|
|
group: 'top',
|
|
keywords: 'logo favicon icon company imprint privacy website domain',
|
|
sections: [
|
|
{ id: 'scope', title: 'Scope', keywords: 'domain host wildcard default' },
|
|
{ id: 'images', title: 'Images and logos', keywords: 'favicon app logo login logo light dark upload' },
|
|
{ id: 'pwa', title: 'Progressive web app', keywords: 'pwa icon screenshot short name description theme colour background colour install' },
|
|
{ id: 'company', title: 'Company information', keywords: 'company name imprint privacy policy website url' },
|
|
],
|
|
},
|
|
{
|
|
id: 'policy',
|
|
label: 'User policy',
|
|
description: 'Which features users get and which settings they may change',
|
|
group: 'top',
|
|
keywords: 'features gates restrictions lock hide users',
|
|
sections: [
|
|
{ id: 'features', title: 'Features', keywords: 'calendar contacts files smime templates keywords sidebar apps unified mailbox archive custom from debug export' },
|
|
{ id: 'push-relays', title: 'Push relays', keywords: 'notifications relay url default lock' },
|
|
{ id: 'sidebar-apps', title: 'Default sidebar apps', keywords: 'apps rail intranet inline tab icon' },
|
|
{ id: 'restrictions', title: 'Locked settings', keywords: 'restrict hide lock font size density layout delete action reply mode session timeout notifications debug' },
|
|
],
|
|
},
|
|
{
|
|
id: 'organisation',
|
|
label: 'Domains and groups',
|
|
description: 'Give some domains or groups their own settings, see what applies to a user, and keep the policy in one file',
|
|
group: 'organisation',
|
|
keywords: 'organisation organization scope domain group department customer tenant effective policy export import file json',
|
|
sections: [
|
|
{ id: 'scopes', title: 'Domains and groups', keywords: 'scope domain group add remove order' },
|
|
{ id: 'effective', title: 'Effective policy', keywords: 'user address lookup applies source' },
|
|
{ id: 'policy-file', title: 'Policy file', keywords: 'export import download upload json backup git' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org-signatures',
|
|
label: 'Signatures',
|
|
description: 'The signature and legal disclaimer every message carries',
|
|
group: 'organisation',
|
|
keywords: 'signature disclaimer footer legal imprint placeholder phone title department lock',
|
|
sections: [
|
|
{ id: 'signature', title: 'Signature', keywords: 'template placeholder name email lock' },
|
|
{ id: 'details', title: 'User details', keywords: 'phone mobile title department fields' },
|
|
{ id: 'disclaimer', title: 'Disclaimer', keywords: 'footer legal notice confidentiality' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org-templates',
|
|
label: 'Templates',
|
|
description: 'Templates every user can pick, and what users may keep of their own',
|
|
group: 'organisation',
|
|
keywords: 'templates library snippets canned responses team personal',
|
|
sections: [
|
|
{ id: 'library', title: 'Organisation library', keywords: 'templates read-only shared add edit' },
|
|
{ id: 'user-templates', title: 'Personal and team templates', keywords: 'personal team group lock hide' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org-security',
|
|
label: 'Security',
|
|
description: 'Warnings about mail from and to outside the organisation, sign-out times and outside connections',
|
|
group: 'organisation',
|
|
keywords: 'security external warning label recipients domains session timeout idle sign out third party traffic privacy',
|
|
sections: [
|
|
{ id: 'external', title: 'External mail', keywords: 'external sender label recipient warning organisation domains contacts' },
|
|
{ id: 'sessions', title: 'Sessions', keywords: 'idle timeout maximum length sign out warning' },
|
|
{ id: 'third-party', title: 'Third-party traffic', keywords: 'telemetry favicon relay directory outgoing hosts privacy gdpr' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org-sharing',
|
|
label: 'Sharing',
|
|
description: 'Tags in shared mailboxes, delegates and recovering deleted items',
|
|
group: 'organisation',
|
|
keywords: 'sharing shared mailbox tags delegate access recover deleted items archive',
|
|
sections: [
|
|
{ id: 'shared-tags', title: 'Shared tags', keywords: 'tags labels colours group mailbox edit lock' },
|
|
{ id: 'delegates', title: 'Delegates', keywords: 'delegate access mail calendar contacts rights read write' },
|
|
{ id: 'recovery', title: 'Recover deleted items', keywords: 'recover restore deleted archive enterprise' },
|
|
],
|
|
},
|
|
{
|
|
id: 'plugins',
|
|
label: 'Plugins',
|
|
description: 'Plugins installed for every user, and what users may install themselves',
|
|
group: 'extensions',
|
|
keywords: 'extensions addons upload zip approve permissions configure force enable',
|
|
sections: [
|
|
{ id: 'approvals', title: 'Approval requests', keywords: 'approve deny user uploaded code review' },
|
|
{ id: 'installed', title: 'Installed plugins', keywords: 'enable disable remove roll back configure' },
|
|
{ id: 'policy', title: 'Plugin policy', keywords: 'plugins enabled user uploads require approval' },
|
|
],
|
|
hasDetail: true,
|
|
},
|
|
{
|
|
id: 'themes',
|
|
label: 'Themes',
|
|
description: 'Themes installed for every user, defaults and what users may pick',
|
|
group: 'extensions',
|
|
keywords: 'colour color mode dark light default upload zip',
|
|
sections: [
|
|
{ id: 'installed', title: 'Installed themes', keywords: 'enable disable remove roll back' },
|
|
{ id: 'policy', title: 'Theme policy', keywords: 'themes enabled user uploads default theme colour mode lock' },
|
|
{ id: 'offered', title: 'Offered to users', keywords: 'built-in themes hide' },
|
|
],
|
|
hasDetail: true,
|
|
},
|
|
{
|
|
id: 'marketplace',
|
|
label: 'Marketplace',
|
|
description: 'Plugins and themes from the extension directory',
|
|
group: 'extensions',
|
|
keywords: 'directory install update browse extensions advisories',
|
|
sections: [],
|
|
hasDetail: true,
|
|
},
|
|
{
|
|
id: 'version',
|
|
label: 'Updates',
|
|
description: 'The running version and the hourly update check',
|
|
group: 'system',
|
|
keywords: 'version release upgrade check security advisory',
|
|
sections: [
|
|
{ id: 'status', title: 'Status', keywords: 'running latest release severity advisory' },
|
|
{ id: 'schedule', title: 'Schedule', keywords: 'last checked next' },
|
|
{ id: 'source', title: 'Source', keywords: 'endpoint disabled env' },
|
|
],
|
|
},
|
|
{
|
|
id: 'telemetry',
|
|
label: 'Usage stats',
|
|
description: 'The anonymous daily heartbeat, off unless you turn it on',
|
|
group: 'system',
|
|
keywords: 'telemetry heartbeat anonymous consent privacy',
|
|
sections: [
|
|
{ id: 'status', title: 'Status', keywords: 'enable disable consent error counts' },
|
|
{ id: 'accounts', title: 'Account activity', keywords: 'active total logins' },
|
|
{ id: 'endpoint', title: 'Endpoint', keywords: 'collector url' },
|
|
{ id: 'payload', title: 'Payload preview', keywords: 'send now json' },
|
|
],
|
|
},
|
|
{
|
|
id: 'monitoring',
|
|
label: 'Monitoring',
|
|
description: 'Error reports, metrics, health checks and tracing for your own tools',
|
|
group: 'system',
|
|
keywords: 'monitoring errors sentry bugsink glitchtip dsn prometheus metrics health probes tracing opentelemetry otel',
|
|
sections: [
|
|
{ id: 'errors', title: 'Error reporting', keywords: 'dsn sentry bugsink glitchtip sample rate environment' },
|
|
{ id: 'errors-today', title: 'Errors today', keywords: 'count fingerprint top' },
|
|
{ id: 'metrics', title: 'Metrics', keywords: 'prometheus scrape token grafana' },
|
|
{ id: 'health', title: 'Health checks', keywords: 'liveness readiness probe kubernetes docker' },
|
|
],
|
|
},
|
|
{
|
|
id: 'logs',
|
|
label: 'Audit log',
|
|
description: 'Who changed what in this panel, and when',
|
|
group: 'system',
|
|
keywords: 'audit history actions login config policy ip',
|
|
sections: [],
|
|
},
|
|
];
|
|
|
|
const PAGE_BY_ID = new Map(ADMIN_PAGES.map((page) => [page.id, page]));
|
|
|
|
export function adminPageDef(id: AdminPageId): AdminPageDef {
|
|
return PAGE_BY_ID.get(id)!;
|
|
}
|
|
|
|
export const ADMIN_ROOT = '/admin';
|
|
|
|
/** Where in the admin panel a pathname points. `page` null is the index. */
|
|
export interface AdminLocation {
|
|
page: AdminPageId | null;
|
|
/** The id or slug of a detail page, for pages that have them. */
|
|
detail: string | null;
|
|
}
|
|
|
|
/**
|
|
* Reads the admin location from a pathname (without the base path, as
|
|
* `usePathname()` gives it). Null when the path is not inside /admin.
|
|
* An unknown page name counts as the index rather than a 404: the login and
|
|
* password routes are separate pages and never get here.
|
|
*/
|
|
export function parseAdminPath(pathname: string): AdminLocation | null {
|
|
if (pathname !== ADMIN_ROOT && !pathname.startsWith(`${ADMIN_ROOT}/`)) return null;
|
|
const segments = pathname
|
|
.slice(ADMIN_ROOT.length)
|
|
.split('/')
|
|
.filter(Boolean)
|
|
.map((segment) => {
|
|
try { return decodeURIComponent(segment); } catch { return segment; }
|
|
});
|
|
if (segments.length === 0) return { page: null, detail: null };
|
|
const [first, ...rest] = segments;
|
|
if (!isAdminPageId(first)) return { page: null, detail: null };
|
|
const def = adminPageDef(first);
|
|
const detail = def.hasDetail && rest.length > 0 ? rest.join('/') : null;
|
|
return { page: first, detail };
|
|
}
|
|
|
|
/** The app-relative path of a page, or of one of its detail pages. */
|
|
export function adminPath(page?: AdminPageId | null, detail?: string | null): string {
|
|
if (!page) return ADMIN_ROOT;
|
|
const base = `${ADMIN_ROOT}/${page}`;
|
|
return detail ? `${base}/${encodeURIComponent(detail)}` : base;
|
|
}
|