feat: add global toggles for TOTP and Email 2FA in system settings, conditionally show/hide user 2FA tab
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user