This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { HelpText } from './HelpText';
|
||||
import { FieldLabel, FieldInput } from './formUtils';
|
||||
import { StringArrayEditor } from './StringArrayEditor';
|
||||
import type { SectionFormProps } from './types';
|
||||
|
||||
/**
|
||||
* Authentication config (`auth.*`): session policy, admin allowlist, and the
|
||||
* Google / Gitea OAuth providers. Secrets (session secret, client secrets) are
|
||||
* server-masked (config-manager SENSITIVE_PATHS) — they render as ******** and
|
||||
* saving without re-entering keeps the stored value.
|
||||
*
|
||||
* Leaving the whole `auth` block empty (no providers) keeps the app in no-auth
|
||||
* mode — every visitor is treated as a local admin.
|
||||
*/
|
||||
export function AuthForm({ config, onChange }: SectionFormProps) {
|
||||
const auth = config.auth ?? {};
|
||||
const providers = auth.providers ?? {};
|
||||
const google = providers.google ?? {};
|
||||
const gitea = providers.gitea ?? {};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<h2 className="text-base font-semibold text-slate-800">Authentication</h2>
|
||||
<p className="text-[13px] text-slate-500">
|
||||
ログイン認証。providers を未設定にすると <strong>認証なし(全員ローカル admin)</strong>で動作する。
|
||||
変更後はサーバ再起動が必要。
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<FieldLabel>Primary Provider</FieldLabel>
|
||||
<select
|
||||
value={auth.primaryProvider ?? ''}
|
||||
onChange={e => onChange('auth.primaryProvider', e.target.value)}
|
||||
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow"
|
||||
>
|
||||
<option value="">(指定なし・両方有効)</option>
|
||||
<option value="google">google のみ</option>
|
||||
<option value="gitea">gitea のみ</option>
|
||||
</select>
|
||||
<HelpText>単一プロバイダーに限定する場合に指定。未指定なら設定済みの全プロバイダーでログイン可</HelpText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FieldLabel>Admin Emails</FieldLabel>
|
||||
<StringArrayEditor
|
||||
value={auth.adminEmails ?? []}
|
||||
onChange={v => onChange('auth.adminEmails', v)}
|
||||
placeholder="[email protected]"
|
||||
/>
|
||||
<HelpText>admin ロールを付与するメールアドレス</HelpText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" checked={auth.secureCookie === true}
|
||||
onChange={e => onChange('auth.secureCookie', e.target.checked)} className="rounded" />
|
||||
Secure Cookie(HTTPS のみで Cookie 送信)
|
||||
</label>
|
||||
<HelpText>本番(HTTPS)では有効推奨。HTTP のローカル開発では無効</HelpText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FieldLabel>Session Max Age (ms)</FieldLabel>
|
||||
<FieldInput type="number" value={auth.sessionMaxAge ?? ''}
|
||||
onChange={v => onChange('auth.sessionMaxAge', v ? Number(v) : undefined)} />
|
||||
<HelpText>セッションの有効期間(ミリ秒)</HelpText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FieldLabel>Session Secret</FieldLabel>
|
||||
<FieldInput type="password" value={auth.sessionSecret ?? ''}
|
||||
onChange={v => onChange('auth.sessionSecret', v)} />
|
||||
<HelpText>セッション署名鍵。十分長いランダム文字列を設定(保存後はマスク表示)</HelpText>
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Google OAuth</h3>
|
||||
<div>
|
||||
<FieldLabel>Client ID</FieldLabel>
|
||||
<FieldInput value={google.clientId ?? ''}
|
||||
onChange={v => onChange('auth.providers.google.clientId', v)} />
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>Client Secret</FieldLabel>
|
||||
<FieldInput type="password" value={google.clientSecret ?? ''}
|
||||
onChange={v => onChange('auth.providers.google.clientSecret', v)} />
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>Callback URL</FieldLabel>
|
||||
<FieldInput value={google.callbackUrl ?? ''} placeholder="https://example.com/auth/google/callback"
|
||||
onChange={v => onChange('auth.providers.google.callbackUrl', v)} />
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Gitea OAuth</h3>
|
||||
<div>
|
||||
<FieldLabel>Base URL</FieldLabel>
|
||||
<FieldInput value={gitea.baseUrl ?? ''} placeholder="https://gitea.example.com"
|
||||
onChange={v => onChange('auth.providers.gitea.baseUrl', v)} />
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>Client ID</FieldLabel>
|
||||
<FieldInput value={gitea.clientId ?? ''}
|
||||
onChange={v => onChange('auth.providers.gitea.clientId', v)} />
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>Client Secret</FieldLabel>
|
||||
<FieldInput type="password" value={gitea.clientSecret ?? ''}
|
||||
onChange={v => onChange('auth.providers.gitea.clientSecret', v)} />
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>Callback URL</FieldLabel>
|
||||
<FieldInput value={gitea.callbackUrl ?? ''} placeholder="https://example.com/auth/gitea/callback"
|
||||
onChange={v => onChange('auth.providers.gitea.callbackUrl', v)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -91,6 +91,13 @@ export function BrowserSettingsForm({ config, onChange }: SectionFormProps) {
|
||||
onChange={v => onChange('browser.maxSessions', Number(v))} />
|
||||
<HelpText>同時に起動できるセッションの最大数。デフォルト: 3</HelpText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FieldLabel>Task Session Idle TTL (秒)</FieldLabel>
|
||||
<FieldInput type="number" value={browser.taskSessionIdleTtl ?? ''}
|
||||
onChange={v => onChange('browser.taskSessionIdleTtl', v ? Number(v) : undefined)} />
|
||||
<HelpText>タスク用 CDP セッションをアイドル後に破棄するまでの秒数(GC)。デフォルト: 300</HelpText>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { SshForm } from './SshForm';
|
||||
import { GatewayServerForm } from './GatewayServerForm';
|
||||
import { NotesForm } from './NotesForm';
|
||||
import { PushNotificationsForm } from './PushNotificationsForm';
|
||||
import { AuthForm } from './AuthForm';
|
||||
|
||||
import { useAuthState } from '../../App';
|
||||
|
||||
@@ -189,6 +190,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
|
||||
case 'reflection': return <ReflectionForm {...formProps} />;
|
||||
case 'notes': return <NotesForm {...formProps} />;
|
||||
case 'push-notifications': return <PushNotificationsForm {...formProps} />;
|
||||
case 'auth': return <AuthForm {...formProps} />;
|
||||
|
||||
// ── Tools sub-sections — Step 9 split the legacy grab-bag
|
||||
// ToolsForm into focused per-category forms. Each binds to the
|
||||
|
||||
@@ -34,6 +34,7 @@ const CONFIG_GROUPS = [
|
||||
{ id: 'branding', label: 'Branding' },
|
||||
{ id: 'paths-storage', label: 'Paths & Storage' },
|
||||
{ id: 'execution', label: 'Execution' },
|
||||
{ id: 'auth', label: 'Authentication' },
|
||||
{ id: 'push-notifications', label: 'Web Push (Server)' },
|
||||
],
|
||||
},
|
||||
@@ -68,6 +69,7 @@ const CONFIG_GROUPS = [
|
||||
{ id: 'tools-browser', label: 'Browser Runtime' },
|
||||
{ id: 'tools-media', label: 'Media & Documents' },
|
||||
{ id: 'tools-external', label: 'External Services' },
|
||||
{ id: 'search-filter', label: 'Search Filter' },
|
||||
{ id: 'tools-legacy-knowledge', label: 'Legacy Knowledge' },
|
||||
],
|
||||
},
|
||||
@@ -102,7 +104,6 @@ export const LEGACY_SECTION_REDIRECT: Record<string, string> = {
|
||||
workspace: 'paths-storage',
|
||||
tools: 'tools-web',
|
||||
'browser-settings': 'tools-browser',
|
||||
'search-filter': 'tools-web',
|
||||
// browser-sessions never had a Settings page in the new layout —
|
||||
// it lives in User Folder. Keep mapping so an old URL still goes
|
||||
// somewhere sensible.
|
||||
|
||||
@@ -63,6 +63,39 @@ export function ToolsExternalForm({ config, onChange }: SectionFormProps) {
|
||||
placeholder="/path/to/chrome/profile" />
|
||||
<HelpText>Cookie 抽出用の Chrome プロファイルディレクトリ。</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>X Media Download</FieldLabel>
|
||||
<select value={tools.xDownloadMedia ?? 'auto'}
|
||||
onChange={e => onChange('tools.xDownloadMedia', e.target.value)}
|
||||
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow">
|
||||
<option value="auto">auto(画像/メディアを自動取得・既定)</option>
|
||||
<option value="never">never(取得しない)</option>
|
||||
</select>
|
||||
<HelpText>X 投稿の画像等メディアの自動ダウンロード。デフォルト: auto</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>X Video Download</FieldLabel>
|
||||
<select value={tools.xDownloadVideo ?? 'thumbnail'}
|
||||
onChange={e => onChange('tools.xDownloadVideo', e.target.value)}
|
||||
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow">
|
||||
<option value="thumbnail">thumbnail(サムネイルのみ・既定)</option>
|
||||
<option value="full">full(動画本体を取得)</option>
|
||||
<option value="never">never(取得しない)</option>
|
||||
</select>
|
||||
<HelpText>X 投稿の動画の取得モード。デフォルト: thumbnail(帯域節約)</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>X Media Max (MB)</FieldLabel>
|
||||
<FieldInput type="number" value={tools.xMediaMaxMb ?? ''}
|
||||
onChange={v => onChange('tools.xMediaMaxMb', v ? Number(v) : undefined)} />
|
||||
<HelpText>1 メディアあたりの最大ダウンロードサイズ(MB)</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>X Media Fetch Timeout (秒)</FieldLabel>
|
||||
<FieldInput type="number" value={tools.xMediaFetchTimeoutSeconds ?? ''}
|
||||
onChange={v => onChange('tools.xMediaFetchTimeoutSeconds', v ? Number(v) : undefined)} />
|
||||
<HelpText>メディア取得のタイムアウト(秒)</HelpText>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-5 pt-2 border-t border-hairline">
|
||||
@@ -74,6 +107,12 @@ export function ToolsExternalForm({ config, onChange }: SectionFormProps) {
|
||||
<FieldInput type="password" value={tools.googleMapsApiKey ?? ''} onChange={v => onChange('tools.googleMapsApiKey', v)} />
|
||||
<HelpText>Google Maps Places / Directions API キー。未設定時は Nominatim / OSRM(無料)を使用。</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>Maps Timeout (秒)</FieldLabel>
|
||||
<FieldInput type="number" value={tools.mapsTimeout ?? ''}
|
||||
onChange={v => onChange('tools.mapsTimeout', v ? Number(v) : undefined)} />
|
||||
<HelpText>Maps / Nominatim / OSRM 呼び出しのタイムアウト(秒)。デフォルト: 30</HelpText>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-5 pt-2 border-t border-hairline">
|
||||
|
||||
Reference in New Issue
Block a user