This commit is contained in:
@@ -23,6 +23,7 @@ import { NotificationsForm } from './NotificationsForm';
|
||||
import { BrandingForm } from './BrandingForm';
|
||||
import { MemoryLearningForm } from './MemoryLearningForm';
|
||||
import { MetricsForm } from './MetricsForm';
|
||||
import { ServerTlsForm } from './ServerTlsForm';
|
||||
import { ReflectionForm } from './ReflectionForm';
|
||||
import { McpForm } from './McpForm';
|
||||
import { SshForm } from './SshForm';
|
||||
@@ -195,6 +196,9 @@ function ConfigFormInner({ section }: ConfigFormProps) {
|
||||
case 'gateway-server': return <GatewayServerForm {...formProps} />;
|
||||
case 'llm-metrics': return <MetricsForm {...formProps} />;
|
||||
|
||||
// ── Server / Network
|
||||
case 'server-tls': return <ServerTlsForm {...formProps} />;
|
||||
|
||||
// ── Agent Runtime
|
||||
case 'ask-subtasks': return <AskSubtasksForm {...formProps} />;
|
||||
case 'context': return <ContextForm {...formProps} />;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HelpText } from './HelpText';
|
||||
import { FieldLabel, FieldInput } from './formUtils';
|
||||
import { StringArrayEditor } from './StringArrayEditor';
|
||||
import type { SectionFormProps } from './types';
|
||||
|
||||
/**
|
||||
* Server TLS settings — binds to the `server.tls` config object.
|
||||
*
|
||||
* Editable fields: enabled, certFile, keyFile, httpRedirect,
|
||||
* httpRedirectPort, redirectHost, selfSignedHosts.
|
||||
*
|
||||
* Fields intentionally omitted from the UI (left to config.yaml):
|
||||
* minVersion, selfSignedDir. The onChange path mechanism in ConfigFormInner
|
||||
* uses setNestedValue which does a shallow-merge, so unedited fields are
|
||||
* preserved on save automatically.
|
||||
*
|
||||
* TODO(server-tls-info): cert source/expiry/fingerprint panel + regenerate
|
||||
* button need a GET /api/server/tls-info endpoint (future).
|
||||
*/
|
||||
export function ServerTlsForm({ config, onChange }: SectionFormProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
// Navigate to the server.tls sub-object; fall back to empty object if absent.
|
||||
const tls = (config?.server?.tls) ?? {};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<h2 className="text-base font-semibold text-slate-800 dark:text-slate-100">
|
||||
{t('serverTls.title')}
|
||||
</h2>
|
||||
|
||||
{/* Restart-required banner — always visible */}
|
||||
<div className="px-3 py-2.5 rounded-md border border-amber-300 bg-amber-50 dark:bg-amber-500/10 dark:border-amber-500/30 text-xs text-amber-800 dark:text-amber-300">
|
||||
{t('serverTls.restartBanner')}
|
||||
</div>
|
||||
|
||||
{/* Enable HTTPS */}
|
||||
<div>
|
||||
<label className="inline-flex items-center gap-2 text-[13px] text-slate-700 dark:text-slate-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tls.enabled === true}
|
||||
onChange={e => onChange('server.tls.enabled', e.target.checked)}
|
||||
/>
|
||||
<span>{t('serverTls.enabled')}</span>
|
||||
</label>
|
||||
<HelpText>{t('serverTls.enabledHelp')}</HelpText>
|
||||
</div>
|
||||
|
||||
{/* Certificate file */}
|
||||
<div>
|
||||
<FieldLabel>{t('serverTls.certFile')}</FieldLabel>
|
||||
<FieldInput
|
||||
value={tls.certFile ?? ''}
|
||||
onChange={v => onChange('server.tls.certFile', v || undefined)}
|
||||
placeholder="/etc/ssl/certs/server.pem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Private key file */}
|
||||
<div>
|
||||
<FieldLabel>{t('serverTls.keyFile')}</FieldLabel>
|
||||
<FieldInput
|
||||
value={tls.keyFile ?? ''}
|
||||
onChange={v => onChange('server.tls.keyFile', v || undefined)}
|
||||
placeholder="/etc/ssl/private/server.key"
|
||||
/>
|
||||
<HelpText>{t('serverTls.certHelp')}</HelpText>
|
||||
</div>
|
||||
|
||||
{/* HTTP → HTTPS redirect */}
|
||||
<div>
|
||||
<label className="inline-flex items-center gap-2 text-[13px] text-slate-700 dark:text-slate-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tls.httpRedirect === true}
|
||||
onChange={e => onChange('server.tls.httpRedirect', e.target.checked)}
|
||||
/>
|
||||
<span>{t('serverTls.httpRedirect')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* HTTP redirect port */}
|
||||
<div>
|
||||
<FieldLabel>{t('serverTls.httpRedirectPort')}</FieldLabel>
|
||||
<FieldInput
|
||||
type="number"
|
||||
value={tls.httpRedirectPort != null ? String(tls.httpRedirectPort) : ''}
|
||||
onChange={v => {
|
||||
const n = parseInt(v, 10);
|
||||
onChange('server.tls.httpRedirectPort', isNaN(n) ? undefined : n);
|
||||
}}
|
||||
placeholder="9080"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Redirect host (optional) */}
|
||||
<div>
|
||||
<FieldLabel>{t('serverTls.redirectHost')}</FieldLabel>
|
||||
<FieldInput
|
||||
value={tls.redirectHost ?? ''}
|
||||
onChange={v => onChange('server.tls.redirectHost', v || undefined)}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Additional SAN hostnames for self-signed cert */}
|
||||
<div>
|
||||
<FieldLabel>{t('serverTls.selfSignedHosts')}</FieldLabel>
|
||||
<StringArrayEditor
|
||||
value={tls.selfSignedHosts ?? []}
|
||||
onChange={v => onChange('server.tls.selfSignedHosts', v)}
|
||||
placeholder="example.com / 10.0.0.10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -90,6 +90,13 @@ const CONFIG_GROUPS = [
|
||||
{ id: 'ssh', label: 'Admin SSH' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Network',
|
||||
adminOnly: true,
|
||||
sections: [
|
||||
{ id: 'server-tls', label: 'HTTPS / TLS' },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user