feat: add global toggles for TOTP and Email 2FA in system settings, conditionally show/hide user 2FA tab

This commit is contained in:
2026-05-21 22:05:14 +07:00
parent 6c582282ac
commit 1a30122c3d
41 changed files with 348 additions and 285 deletions
+149 -126
View File
@@ -17,6 +17,10 @@ registerPlugin(FilePondPluginImagePreview, FilePondPluginFileValidateType);
interface SettingsProps extends PageProps {
mustVerifyEmail: boolean;
status?: string;
twoFactorSettings: {
totp_allowed: boolean;
email_allowed: boolean;
};
twoFactor: {
enabled: boolean;
qr_code: string | null;
@@ -66,15 +70,28 @@ function getSettingsTabFromHash(): SettingsTab {
return SETTINGS_TABS.includes(hash) ? hash : 'profile';
}
export default function SettingsIndex({ twoFactor }: SettingsProps) {
export default function SettingsIndex({ twoFactor, twoFactorSettings }: SettingsProps) {
const { user } = usePage<PageProps>().props.auth;
const [activeTab, setActiveTab] = useState<SettingsTab>(getSettingsTabFromHash);
const [activeTab, setActiveTab] = useState<SettingsTab>(() => {
const hash = getSettingsTabFromHash();
if (hash === '2fa' && !(twoFactorSettings.totp_allowed || twoFactorSettings.email_allowed)) {
return 'profile';
}
return hash;
});
useEffect(() => {
const onHashChange = () => setActiveTab(getSettingsTabFromHash());
const onHashChange = () => {
const hash = getSettingsTabFromHash();
if (hash === '2fa' && !(twoFactorSettings.totp_allowed || twoFactorSettings.email_allowed)) {
setActiveTab('profile');
} else {
setActiveTab(hash);
}
};
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
}, []);
}, [twoFactorSettings]);
const switchTab = (tab: SettingsTab) => {
window.location.hash = tab;
@@ -260,11 +277,13 @@ export default function SettingsIndex({ twoFactor }: SettingsProps) {
Security & Password
{activeTab === 'security' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
</button>
<button type="button" onClick={() => switchTab('2fa')}
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === '2fa' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
Two-Factor Auth
{activeTab === '2fa' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
</button>
{(twoFactorSettings.totp_allowed || twoFactorSettings.email_allowed) && (
<button type="button" onClick={() => switchTab('2fa')}
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === '2fa' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
Two-Factor Auth
{activeTab === '2fa' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
</button>
)}
<button type="button" onClick={() => switchTab('danger')}
className={`relative pb-3 px-1 text-sm font-bold tracking-tight transition-colors ${activeTab === 'danger' ? 'text-red-600' : 'text-gray-400 hover:text-red-600'}`}>
Danger Zone
@@ -380,136 +399,140 @@ export default function SettingsIndex({ twoFactor }: SettingsProps) {
</div>
</div>
{!twoFactor.enabled ? (
<SectionCard title="Setup Authenticator App" description="Scan QR code dengan Google Authenticator, Authy, atau TOTP app lainnya">
<div className="flex flex-col md:flex-row gap-10 items-center md:items-start">
<div className="shrink-0">
<div className="p-4 bg-white border-2 border-gray-100 rounded-2xl inline-block shadow-sm">
{twoFactor.qr_code && <img src={twoFactor.qr_code} alt="2FA QR Code" className="w-44 h-44" />}
{twoFactorSettings.totp_allowed && (
!twoFactor.enabled ? (
<SectionCard title="Setup Authenticator App" description="Scan QR code dengan Google Authenticator, Authy, atau TOTP app lainnya">
<div className="flex flex-col md:flex-row gap-10 items-center md:items-start">
<div className="shrink-0">
<div className="p-4 bg-white border-2 border-gray-100 rounded-2xl inline-block shadow-sm">
{twoFactor.qr_code && <img src={twoFactor.qr_code} alt="2FA QR Code" className="w-44 h-44" />}
</div>
</div>
</div>
<div className="flex-1 space-y-6">
<div className="space-y-3">
{[
'Install aplikasi authenticator (Google Authenticator, Authy, 1Password)',
'Scan QR code atau masukkan manual key di bawah',
'Masukkan kode 6 digit dari aplikasi untuk mengaktifkan',
].map((step, i) => (
<div key={i} className="flex items-start gap-3">
<div className="w-6 h-6 rounded-full bg-[#3D4E4B] text-white text-[10px] font-black flex items-center justify-center shrink-0 mt-0.5">{i + 1}</div>
<p className="text-xs font-medium text-gray-500 leading-relaxed pt-0.5">{step}</p>
<div className="flex-1 space-y-6">
<div className="space-y-3">
{[
'Install aplikasi authenticator (Google Authenticator, Authy, 1Password)',
'Scan QR code atau masukkan manual key di bawah',
'Masukkan kode 6 digit dari aplikasi untuk mengaktifkan',
].map((step, i) => (
<div key={i} className="flex items-start gap-3">
<div className="w-6 h-6 rounded-full bg-[#3D4E4B] text-white text-[10px] font-black flex items-center justify-center shrink-0 mt-0.5">{i + 1}</div>
<p className="text-xs font-medium text-gray-500 leading-relaxed pt-0.5">{step}</p>
</div>
))}
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Manual Key</label>
<div className="flex items-center gap-2">
<code className="flex-1 text-xs font-mono bg-gray-50 border border-gray-100 rounded-xl px-4 py-3 text-[#3D4E4B] tracking-wider">
{twoFactor.secret}
</code>
<button onClick={copySecret} type="button"
className="shrink-0 h-11 px-4 border border-gray-200 rounded-xl text-xs font-bold text-gray-500 hover:bg-gray-50 transition-all">
{copiedSecret ? '✓ Copied' : 'Copy'}
</button>
</div>
))}
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Manual Key</label>
<div className="flex items-center gap-2">
<code className="flex-1 text-xs font-mono bg-gray-50 border border-gray-100 rounded-xl px-4 py-3 text-[#3D4E4B] tracking-wider">
{twoFactor.secret}
</code>
<button onClick={copySecret} type="button"
className="shrink-0 h-11 px-4 border border-gray-200 rounded-xl text-xs font-bold text-gray-500 hover:bg-gray-50 transition-all">
{copiedSecret ? '✓ Copied' : 'Copy'}
</div>
<form onSubmit={handleEnable2FA} className="space-y-3">
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Verification Code *</label>
<input type="text" inputMode="numeric" maxLength={6}
value={twoFactorForm.data.code}
onChange={e => twoFactorForm.setData('code', e.target.value)}
className="input-field w-full text-center tracking-[0.5em] font-bold text-lg"
placeholder="000000" />
{twoFactorForm.errors.code && <p className="text-xs text-red-500 font-semibold mt-1">{twoFactorForm.errors.code}</p>}
</div>
<button type="submit" disabled={twoFactorForm.processing || twoFactorForm.data.code.length < 6}
className="w-full h-11 bg-[#3D4E4B] text-white text-xs font-bold rounded-xl hover:bg-[#2D3A38] transition-all disabled:opacity-60">
{twoFactorForm.processing ? 'Verifying...' : 'Enable Two-Factor Authentication'}
</button>
</div>
</form>
</div>
<form onSubmit={handleEnable2FA} className="space-y-3">
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Verification Code *</label>
<input type="text" inputMode="numeric" maxLength={6}
value={twoFactorForm.data.code}
onChange={e => twoFactorForm.setData('code', e.target.value)}
className="input-field w-full text-center tracking-[0.5em] font-bold text-lg"
placeholder="000000" />
{twoFactorForm.errors.code && <p className="text-xs text-red-500 font-semibold mt-1">{twoFactorForm.errors.code}</p>}
</div>
<button type="submit" disabled={twoFactorForm.processing || twoFactorForm.data.code.length < 6}
className="w-full h-11 bg-[#3D4E4B] text-white text-xs font-bold rounded-xl hover:bg-[#2D3A38] transition-all disabled:opacity-60">
{twoFactorForm.processing ? 'Verifying...' : 'Enable Two-Factor Authentication'}
</button>
</form>
</div>
</div>
</SectionCard>
) : (
<div className="space-y-4">
<div className="bg-white rounded-2xl border border-emerald-100 shadow-sm p-6 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-emerald-50 flex items-center justify-center shrink-0">
<svg className="w-6 h-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div className="flex-1">
<div className="text-sm font-bold text-[#3D4E4B]">Two-Factor Authentication Aktif</div>
<div className="text-xs text-gray-400 font-medium mt-0.5">Akun Anda dilindungi dengan TOTP authentication.</div>
</div>
<button onClick={handleDisable2FA} type="button"
className="h-9 px-5 text-xs font-bold text-red-500 border border-red-200 rounded-xl hover:bg-red-50 transition-all">
Disable 2FA
</button>
</div>
<SectionCard title="Recovery Codes" description="Simpan kode ini dengan aman — gunakan jika kehilangan akses ke authenticator">
<div className="space-y-4">
<div className="flex items-center gap-2">
<button onClick={() => setShowCodes(!showCodes)} type="button"
className="h-8 px-4 text-xs font-bold text-[#3D4E4B] border border-gray-200 rounded-lg hover:bg-gray-50 transition-all">
{showCodes ? 'Hide' : 'Show Codes'}
</button>
<button onClick={handleRegenerate} type="button"
className="h-8 px-4 text-xs font-bold text-[#D4A017] border border-amber-200 rounded-lg hover:bg-amber-50 transition-all">
Regenerate
</button>
</div>
{showCodes && twoFactor.recovery_codes.length > 0 && (
<div>
<div className="grid grid-cols-2 gap-2">
{twoFactor.recovery_codes.map((code, i) => (
<code key={i} className="px-4 py-2.5 bg-gray-50 border border-gray-100 rounded-xl text-xs font-mono text-[#3D4E4B] tracking-wider text-center">
{code}
</code>
))}
</div>
<p className="text-[11px] text-amber-600 font-semibold mt-3"> Setiap kode hanya bisa digunakan sekali.</p>
</div>
)}
</div>
</SectionCard>
</div>
) : (
<div className="space-y-4">
<div className="bg-white rounded-2xl border border-emerald-100 shadow-sm p-6 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-emerald-50 flex items-center justify-center shrink-0">
<svg className="w-6 h-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div className="flex-1">
<div className="text-sm font-bold text-[#3D4E4B]">Two-Factor Authentication Aktif</div>
<div className="text-xs text-gray-400 font-medium mt-0.5">Akun Anda dilindungi dengan TOTP authentication.</div>
</div>
<button onClick={handleDisable2FA} type="button"
className="h-9 px-5 text-xs font-bold text-red-500 border border-red-200 rounded-xl hover:bg-red-50 transition-all">
Disable 2FA
</button>
</div>
<SectionCard title="Recovery Codes" description="Simpan kode ini dengan aman — gunakan jika kehilangan akses ke authenticator">
<div className="space-y-4">
<div className="flex items-center gap-2">
<button onClick={() => setShowCodes(!showCodes)} type="button"
className="h-8 px-4 text-xs font-bold text-[#3D4E4B] border border-gray-200 rounded-lg hover:bg-gray-50 transition-all">
{showCodes ? 'Hide' : 'Show Codes'}
</button>
<button onClick={handleRegenerate} type="button"
className="h-8 px-4 text-xs font-bold text-[#D4A017] border border-amber-200 rounded-lg hover:bg-amber-50 transition-all">
Regenerate
</button>
</div>
{showCodes && twoFactor.recovery_codes.length > 0 && (
<div>
<div className="grid grid-cols-2 gap-2">
{twoFactor.recovery_codes.map((code, i) => (
<code key={i} className="px-4 py-2.5 bg-gray-50 border border-gray-100 rounded-xl text-xs font-mono text-[#3D4E4B] tracking-wider text-center">
{code}
</code>
))}
</div>
<p className="text-[11px] text-amber-600 font-semibold mt-3"> Setiap kode hanya bisa digunakan sekali.</p>
</div>
)}
</div>
</SectionCard>
</div>
)
)}
{/* Email 2FA Setup */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center shrink-0">
<svg className="w-6 h-6 text-[#D4A017]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div className="flex-1">
<div className="text-sm font-bold text-[#3D4E4B]">Two-Factor Authentication via Email</div>
<div className="text-xs text-gray-400 font-medium mt-0.5">
{twoFactor.email_enabled
? 'Email 2FA is active. Every login attempt will require an OTP code sent to your email.'
: 'Receive a 6-digit OTP code in your email to secure your login sessions.'}
{twoFactorSettings.email_allowed && (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center shrink-0">
<svg className="w-6 h-6 text-[#D4A017]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
{!twoFactor.smtp_configured && (
<div className="flex items-center gap-1.5 mt-2 text-[11px] font-bold text-amber-600 bg-amber-50 px-2.5 py-1 rounded-lg w-fit border border-amber-200">
<span className="text-sm"></span>
<span>SMTP Mail Server is not configured. Setup required.</span>
<div className="flex-1">
<div className="text-sm font-bold text-[#3D4E4B]">Two-Factor Authentication via Email</div>
<div className="text-xs text-gray-400 font-medium mt-0.5">
{twoFactor.email_enabled
? 'Email 2FA is active. Every login attempt will require an OTP code sent to your email.'
: 'Receive a 6-digit OTP code in your email to secure your login sessions.'}
</div>
{!twoFactor.smtp_configured && (
<div className="flex items-center gap-1.5 mt-2 text-[11px] font-bold text-amber-600 bg-amber-50 px-2.5 py-1 rounded-lg w-fit border border-amber-200">
<span className="text-sm"></span>
<span>SMTP Mail Server is not configured. Setup required.</span>
</div>
)}
</div>
{twoFactor.email_enabled ? (
<button onClick={() => handleToggleEmail2FA(false)} type="button"
className="h-9 px-5 text-xs font-bold text-red-500 border border-red-200 rounded-xl hover:bg-red-50 transition-all">
Disable Email 2FA
</button>
) : (
<button onClick={() => handleToggleEmail2FA(true)} type="button"
className="h-9 px-5 text-xs font-bold text-[#3D4E4B] border border-gray-200 rounded-xl hover:bg-gray-50 transition-all">
Enable Email 2FA
</button>
)}
</div>
{twoFactor.email_enabled ? (
<button onClick={() => handleToggleEmail2FA(false)} type="button"
className="h-9 px-5 text-xs font-bold text-red-500 border border-red-200 rounded-xl hover:bg-red-50 transition-all">
Disable Email 2FA
</button>
) : (
<button onClick={() => handleToggleEmail2FA(true)} type="button"
className="h-9 px-5 text-xs font-bold text-[#3D4E4B] border border-gray-200 rounded-xl hover:bg-gray-50 transition-all">
Enable Email 2FA
</button>
)}
</div>
)}
</div>
)}
+47 -34
View File
@@ -126,6 +126,8 @@ export default function SystemSettings({ settings }: SystemSettingsProps) {
password_require_symbols: settings.password_require_symbols === '1' || settings.password_require_symbols === true,
password_require_numbers: settings.password_require_numbers === '1' || settings.password_require_numbers === true,
password_require_mixed_case: settings.password_require_mixed_case === '1' || settings.password_require_mixed_case === true,
two_factor_totp_enabled: settings.two_factor_totp_enabled === '1' || settings.two_factor_totp_enabled === true,
two_factor_email_enabled: settings.two_factor_email_enabled === '1' || settings.two_factor_email_enabled === true,
oauth_google_enabled: settings.oauth_google_enabled === '1' || settings.oauth_google_enabled === true,
oauth_google_client_id: settings.oauth_google_client_id || '',
oauth_google_client_secret: settings.oauth_google_client_secret || '',
@@ -237,41 +239,52 @@ export default function SystemSettings({ settings }: SystemSettingsProps) {
)}
{activeTab === 'security' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 anim-fade">
<SectionCard title="Password Standards" description="Complexity requirements for user access" delay="0.1s">
<div className="space-y-2">
<ToggleItem label="Require Symbols" description="Include special characters (!@#$)" checked={data.settings.password_require_symbols} onChange={v => handleChange('password_require_symbols', v)} />
<ToggleItem label="Require Numbers" description="Include numerical digits (0-9)" checked={data.settings.password_require_numbers} onChange={v => handleChange('password_require_numbers', v)} />
<ToggleItem label="Require Mixed Case" description="Force Uppercase and Lowercase letters" checked={data.settings.password_require_mixed_case} onChange={v => handleChange('password_require_mixed_case', v)} />
<div className="flex items-center justify-between py-4 mt-2 border-t border-gray-50">
<span className="text-sm font-bold text-[#3D4E4B]">Minimum Length</span>
<input type="number" value={data.settings.password_minimum_length} onChange={e => handleChange('password_minimum_length', parseInt(e.target.value))} className="w-16 h-10 input-field text-center px-2 text-[#D4A017] font-bold" />
</div>
</div>
</SectionCard>
<SectionCard title="OAuth Providers" description="Third-party authentication protocols" delay="0.15s">
<div className="space-y-6">
{['google', 'github'].map(prov => (
<div key={prov} className="p-6 rounded-2xl bg-gray-50/50 border border-gray-100">
<div className="flex items-center justify-between mb-6">
<div className="text-sm font-bold text-[#3D4E4B] tracking-tight capitalize flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${data.settings[`oauth_${prov}_enabled` as keyof typeof data.settings] ? 'bg-green-500' : 'bg-gray-300'}`} />
{prov} Login
</div>
<label className="relative inline-flex items-center cursor-pointer scale-90">
<input type="checkbox" className="sr-only peer" checked={data.settings[`oauth_${prov}_enabled` as keyof typeof data.settings]} onChange={e => handleChange(`oauth_${prov}_enabled`, e.target.checked)} />
<div className="w-10 h-5 bg-gray-300 rounded-full peer peer-checked:bg-[#D4A017] after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-400 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full border border-gray-200"></div>
</label>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InputField label="Client ID" value={data.settings[`oauth_${prov}_client_id` as keyof typeof data.settings]} onChange={(v:any) => handleChange(`oauth_${prov}_client_id`, v)} placeholder={`Enter ${prov} Client ID`} />
<InputField label="Client Secret" type="password" value={data.settings[`oauth_${prov}_client_secret` as keyof typeof data.settings]} onChange={(v:any) => handleChange(`oauth_${prov}_client_secret`, v)} placeholder="••••••••" />
</div>
<div className="space-y-8 anim-fade">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<SectionCard title="Password Standards" description="Complexity requirements for user access" delay="0.1s">
<div className="space-y-2">
<ToggleItem label="Require Symbols" description="Include special characters (!@#$)" checked={data.settings.password_require_symbols} onChange={v => handleChange('password_require_symbols', v)} />
<ToggleItem label="Require Numbers" description="Include numerical digits (0-9)" checked={data.settings.password_require_numbers} onChange={v => handleChange('password_require_numbers', v)} />
<ToggleItem label="Require Mixed Case" description="Force Uppercase and Lowercase letters" checked={data.settings.password_require_mixed_case} onChange={v => handleChange('password_require_mixed_case', v)} />
<div className="flex items-center justify-between py-4 mt-2 border-t border-gray-50">
<span className="text-sm font-bold text-[#3D4E4B]">Minimum Length</span>
<input type="number" value={data.settings.password_minimum_length} onChange={e => handleChange('password_minimum_length', parseInt(e.target.value))} className="w-16 h-10 input-field text-center px-2 text-[#D4A017] font-bold" />
</div>
))}
</div>
</SectionCard>
</div>
</SectionCard>
<SectionCard title="Two-Factor Authentication (2FA)" description="Configure globally available 2FA options for users" delay="0.12s">
<div className="space-y-2">
<ToggleItem label="Google Authenticator (TOTP)" description="Allow users to use Authenticator Apps (Google, Authy, etc.)" checked={data.settings.two_factor_totp_enabled} onChange={v => handleChange('two_factor_totp_enabled', v)} />
<ToggleItem label="Email 2FA" description="Allow users to receive 6-digit OTP codes via email (requires SMTP configuration)" checked={data.settings.two_factor_email_enabled} onChange={v => handleChange('two_factor_email_enabled', v)} />
</div>
</SectionCard>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<SectionCard title="OAuth Providers" description="Third-party authentication protocols" delay="0.15s">
<div className="space-y-6">
{['google', 'github'].map(prov => (
<div key={prov} className="p-6 rounded-2xl bg-gray-50/50 border border-gray-100">
<div className="flex items-center justify-between mb-6">
<div className="text-sm font-bold text-[#3D4E4B] tracking-tight capitalize flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${data.settings[`oauth_${prov}_enabled` as keyof typeof data.settings] ? 'bg-green-500' : 'bg-gray-300'}`} />
{prov} Login
</div>
<label className="relative inline-flex items-center cursor-pointer scale-90">
<input type="checkbox" className="sr-only peer" checked={data.settings[`oauth_${prov}_enabled` as keyof typeof data.settings]} onChange={e => handleChange(`oauth_${prov}_enabled`, e.target.checked)} />
<div className="w-10 h-5 bg-gray-300 rounded-full peer peer-checked:bg-[#D4A017] after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-400 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full border border-gray-200"></div>
</label>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InputField label="Client ID" value={data.settings[`oauth_${prov}_client_id` as keyof typeof data.settings]} onChange={(v:any) => handleChange(`oauth_${prov}_client_id`, v)} placeholder={`Enter ${prov} Client ID`} />
<InputField label="Client Secret" type="password" value={data.settings[`oauth_${prov}_client_secret` as keyof typeof data.settings]} onChange={(v:any) => handleChange(`oauth_${prov}_client_secret`, v)} placeholder="••••••••" />
</div>
</div>
))}
</div>
</SectionCard>
</div>
</div>
)}