543 lines
34 KiB
TypeScript
543 lines
34 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
|
import { Head, usePage, useForm, router } from '@inertiajs/react';
|
|
import { PageProps } from '@/types';
|
|
import Swal from 'sweetalert2';
|
|
import { swal } from '@/lib/swal';
|
|
|
|
// FilePond
|
|
import { FilePond, registerPlugin } from 'react-filepond';
|
|
import 'filepond/dist/filepond.min.css';
|
|
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
|
|
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css';
|
|
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
|
|
|
|
registerPlugin(FilePondPluginImagePreview, FilePondPluginFileValidateType);
|
|
|
|
interface SettingsProps extends PageProps {
|
|
mustVerifyEmail: boolean;
|
|
status?: string;
|
|
twoFactor: {
|
|
enabled: boolean;
|
|
qr_code: string | null;
|
|
secret: string | null;
|
|
email_enabled: boolean;
|
|
smtp_configured: boolean;
|
|
recovery_codes: string[];
|
|
};
|
|
}
|
|
|
|
/* ─── Reusable Components from System Settings ─────────────────── */
|
|
|
|
function SectionCard({ title, description, children, delay = '0s', variant = 'default' }: { title: string; description: string; children: React.ReactNode; delay?: string; variant?: 'default' | 'danger' }) {
|
|
const isDanger = variant === 'danger';
|
|
|
|
return (
|
|
<div className={`bg-white rounded-2xl border ${isDanger ? 'border-red-200 shadow-none' : 'border-gray-100 shadow-sm'} overflow-hidden h-full flex flex-col anim-up`} style={{ animationDelay: delay }}>
|
|
<div className={`px-6 py-4 border-b ${isDanger ? 'border-red-100 bg-red-50' : 'border-gray-50 bg-gray-50/30'}`}>
|
|
<h2 className={`text-sm font-bold tracking-tight ${isDanger ? 'text-red-600' : 'text-[#3D4E4B]'}`}>{title}</h2>
|
|
<p className={`text-xs font-semibold tracking-tight mt-1 ${isDanger ? 'text-red-400' : 'text-gray-400'}`}>{description}</p>
|
|
</div>
|
|
<div className="p-8 flex-1">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InputField({ label, id, type = 'text', value, onChange, error, placeholder, required = false }: any) {
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<label htmlFor={id} className="block text-xs font-semibold text-gray-500 tracking-tight ml-1">
|
|
{label} {required && <span className="text-red-500">*</span>}
|
|
</label>
|
|
<input
|
|
id={id} type={type} value={value} onChange={onChange} placeholder={placeholder}
|
|
className={`input-field${error ? ' is-error' : ''}`}
|
|
/>
|
|
{error && <p className="text-xs text-red-500 font-semibold ml-1 mt-1">{error}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const SETTINGS_TABS = ['profile', 'security', '2fa', 'danger'] as const;
|
|
type SettingsTab = typeof SETTINGS_TABS[number];
|
|
|
|
function getSettingsTabFromHash(): SettingsTab {
|
|
const hash = window.location.hash.replace('#', '') as SettingsTab;
|
|
return SETTINGS_TABS.includes(hash) ? hash : 'profile';
|
|
}
|
|
|
|
export default function SettingsIndex({ twoFactor }: SettingsProps) {
|
|
const { user } = usePage<PageProps>().props.auth;
|
|
const [activeTab, setActiveTab] = useState<SettingsTab>(getSettingsTabFromHash);
|
|
|
|
useEffect(() => {
|
|
const onHashChange = () => setActiveTab(getSettingsTabFromHash());
|
|
window.addEventListener('hashchange', onHashChange);
|
|
return () => window.removeEventListener('hashchange', onHashChange);
|
|
}, []);
|
|
|
|
const switchTab = (tab: SettingsTab) => {
|
|
window.location.hash = tab;
|
|
setActiveTab(tab);
|
|
};
|
|
|
|
const { data: profileData, setData: setProfileData, post: postProfile, processing: profileProcessing, errors: profileErrors } = useForm({
|
|
first_name: user.first_name || '',
|
|
last_name: user.last_name || '',
|
|
email: user.email || '',
|
|
phone: (user as any).phone || '',
|
|
bio: (user as any).bio || '',
|
|
avatar_file: null as File | null,
|
|
_method: 'PATCH',
|
|
});
|
|
|
|
const [avatarFiles, setAvatarFiles] = useState<any[]>([]);
|
|
|
|
const handleProfileSubmit = (e: React.SyntheticEvent) => {
|
|
e.preventDefault();
|
|
postProfile(route('profile.update'), {
|
|
preserveScroll: true,
|
|
onSuccess: () => swal.success('Saved', 'Profile updated successfully.'),
|
|
});
|
|
};
|
|
|
|
const { data: passwordData, setData: setPasswordData, put: putPassword, processing: passwordProcessing, errors: passwordErrors, reset: resetPassword } = useForm({
|
|
current_password: '',
|
|
password: '',
|
|
password_confirmation: '',
|
|
});
|
|
|
|
const handlePasswordSubmit = (e: React.SyntheticEvent) => {
|
|
e.preventDefault();
|
|
putPassword(route('password.update'), {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
resetPassword();
|
|
swal.success('Saved', 'Password updated successfully.');
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleDeleteAccount = async () => {
|
|
const result = await swal.confirm('Delete account?', 'This action is irreversible.', 'Delete Permanently');
|
|
if (result.isConfirmed) {
|
|
const { value: password } = await Swal.fire({
|
|
title: 'Confirm Password',
|
|
input: 'password',
|
|
inputPlaceholder: 'Enter your current password',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Delete',
|
|
confirmButtonColor: '#dc2626',
|
|
});
|
|
if (password) {
|
|
router.delete(route('profile.destroy'), {
|
|
data: { password }, preserveScroll: true,
|
|
onSuccess: () => swal.success('Deleted', 'Account removed.'),
|
|
onError: (errs) => swal.error('Error', errs.password || 'Incorrect password.'),
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const initials = `${user.first_name?.charAt(0) || ''}${user.last_name?.charAt(0) || ''}`.toUpperCase();
|
|
|
|
// 2FA
|
|
const [copiedSecret, setCopiedSecret] = useState(false);
|
|
const [showCodes, setShowCodes] = useState(false);
|
|
const twoFactorForm = useForm({ code: '' });
|
|
|
|
const copySecret = () => {
|
|
navigator.clipboard.writeText(twoFactor.secret || '');
|
|
setCopiedSecret(true);
|
|
setTimeout(() => setCopiedSecret(false), 2000);
|
|
};
|
|
|
|
const handleEnable2FA = async (e: React.SyntheticEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (twoFactor.email_enabled) {
|
|
const warning = await swal.confirm(
|
|
'Deactivate Email 2FA?',
|
|
'Enabling Google Authenticator will automatically deactivate Email Two-Factor Authentication. Do you want to proceed?',
|
|
'Proceed'
|
|
);
|
|
if (!warning.isConfirmed) return;
|
|
}
|
|
|
|
twoFactorForm.post(route('two-factor.enable'), {
|
|
preserveScroll: true,
|
|
onSuccess: () => { twoFactorForm.reset(); swal.success('Enabled', '2FA is now active on your account.'); },
|
|
});
|
|
};
|
|
|
|
const handleDisable2FA = async () => {
|
|
const { value: password } = await Swal.fire({
|
|
title: 'Disable 2FA',
|
|
text: 'Enter your password to confirm.',
|
|
input: 'password',
|
|
inputPlaceholder: 'Your current password',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Disable',
|
|
confirmButtonColor: '#dc2626',
|
|
});
|
|
if (password) {
|
|
router.post(route('two-factor.disable'), { password }, {
|
|
preserveScroll: true,
|
|
onSuccess: () => swal.success('Disabled', '2FA has been disabled.'),
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleRegenerate = async () => {
|
|
const result = await swal.confirm('Regenerate Codes?', 'Old recovery codes will be invalidated.', 'Regenerate');
|
|
if (result.isConfirmed) {
|
|
router.post(route('two-factor.recovery-codes'), {}, {
|
|
preserveScroll: true,
|
|
onSuccess: () => { setShowCodes(true); swal.success('Regenerated', 'New recovery codes generated.'); },
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleToggleEmail2FA = async (enable: boolean) => {
|
|
if (enable && !twoFactor.smtp_configured) {
|
|
Swal.fire({
|
|
icon: 'warning',
|
|
title: 'SMTP Mail Server Not Set Up',
|
|
text: 'Two-Factor Authentication via Email cannot be enabled because the system SMTP configurations are not set up yet. Please configure the SMTP settings under System Settings (or contact your Administrator) first.',
|
|
confirmButtonColor: '#3D4E4B',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (enable && twoFactor.enabled) {
|
|
const warning = await swal.confirm(
|
|
'Switch to Email 2FA?',
|
|
'Enabling Email 2FA will automatically deactivate your Google Authenticator setup. Do you want to proceed?',
|
|
'Proceed'
|
|
);
|
|
if (!warning.isConfirmed) return;
|
|
}
|
|
|
|
const { value: password } = await Swal.fire({
|
|
title: enable ? 'Enable Email 2FA' : 'Disable Email 2FA',
|
|
text: 'Enter your password to confirm.',
|
|
input: 'password',
|
|
inputPlaceholder: 'Your current password',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Confirm',
|
|
confirmButtonColor: '#3D4E4B',
|
|
});
|
|
if (password) {
|
|
router.post(route('two-factor.email.toggle'), { password, enabled: enable }, {
|
|
preserveScroll: true,
|
|
onSuccess: () => swal.success('Success', `Email Two-Factor Authentication has been ${enable ? 'enabled' : 'disabled'} successfully.`),
|
|
onError: (errs) => swal.error('Error', errs.password || 'Incorrect password.'),
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AuthenticatedLayout>
|
|
<Head title="Account Settings" />
|
|
|
|
{/* Header Section */}
|
|
<div className="flex items-center justify-between mb-8 anim-down">
|
|
<div>
|
|
<h1 className="text-xl font-bold text-[#3D4E4B] tracking-tight leading-none">Account Settings</h1>
|
|
<p className="text-sm font-semibold text-gray-400 tracking-tight mt-2">Manage your personal credentials and security preferences</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs Row */}
|
|
<div className="flex items-center gap-1 border-b border-gray-100 w-full mb-8 anim-down" style={{ animationDelay: '0.05s' }}>
|
|
<button type="button" onClick={() => switchTab('profile')}
|
|
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === 'profile' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
|
|
Profile Information
|
|
{activeTab === 'profile' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
|
|
</button>
|
|
<button type="button" onClick={() => switchTab('security')}
|
|
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === 'security' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
|
|
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>
|
|
<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
|
|
{activeTab === 'danger' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-red-500 rounded-t-full" />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
<div className="space-y-8 pb-20">
|
|
{activeTab === 'profile' && (
|
|
<div className="space-y-8 anim-fade">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<div className="lg:col-span-2">
|
|
<SectionCard title="Identity" description="Manage your public name and contact email" delay="0.1s">
|
|
<form onSubmit={handleProfileSubmit} className="space-y-8">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
|
<InputField label="First Name" id="first_name" value={profileData.first_name}
|
|
onChange={(e: any) => setProfileData('first_name', e.target.value)}
|
|
error={profileErrors.first_name} required placeholder="e.g. John" />
|
|
<InputField label="Last Name" id="last_name" value={profileData.last_name}
|
|
onChange={(e: any) => setProfileData('last_name', e.target.value)}
|
|
error={profileErrors.last_name} required placeholder="e.g. Doe" />
|
|
</div>
|
|
<InputField label="Email Address" id="email" type="email" value={profileData.email}
|
|
onChange={(e: any) => setProfileData('email', e.target.value)}
|
|
error={profileErrors.email} required placeholder="e.g. john.doe@example.com" />
|
|
<InputField label="Phone Number" id="phone" value={profileData.phone}
|
|
onChange={(e: any) => setProfileData('phone', e.target.value)}
|
|
error={(profileErrors as any).phone} placeholder="e.g. +62 812 3456 7890" />
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="bio" className="block text-xs font-semibold text-gray-500 tracking-tight ml-1">Bio</label>
|
|
<textarea
|
|
id="bio"
|
|
value={profileData.bio}
|
|
onChange={(e: any) => setProfileData('bio', e.target.value)}
|
|
rows={3}
|
|
placeholder="A short description about yourself..."
|
|
className={`input-field py-3 resize-none${(profileErrors as any).bio ? ' is-error' : ''}`}
|
|
/>
|
|
{(profileErrors as any).bio && <p className="text-xs text-red-500 font-semibold ml-1 mt-1">{(profileErrors as any).bio}</p>}
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4 border-t border-gray-50">
|
|
<button type="submit" disabled={profileProcessing}
|
|
className="h-10 px-8 bg-[#3D4E4B] text-white text-sm font-bold tracking-tight rounded-xl hover:bg-[#2D3A38] transition-all shadow-lg shadow-[#3D4E4B]/20 disabled:opacity-60">
|
|
{profileProcessing ? 'Saving...' : 'Save Profile'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</SectionCard>
|
|
</div>
|
|
|
|
<div className="lg:col-span-1">
|
|
<SectionCard title="Profile Photo" description="Update your avatar" delay="0.15s">
|
|
<div className="flex flex-col items-center gap-6">
|
|
<div className="w-24 h-24 rounded-2xl bg-[#3D4E4B] flex items-center justify-center text-white text-2xl font-bold shadow-lg shadow-[#3D4E4B]/10 overflow-hidden border-2 border-white">
|
|
{user.avatar_url ? <img src={user.avatar_url} className="w-full h-full object-cover" /> : initials}
|
|
</div>
|
|
<div className="w-full">
|
|
<FilePond files={avatarFiles} onupdatefiles={items => { setAvatarFiles(items); setProfileData('avatar_file', items[0]?.file as File || null); }}
|
|
allowMultiple={false} maxFiles={1} labelIdle='Drop Photo here' />
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'security' && (
|
|
<div className="max-w-4xl anim-fade">
|
|
<SectionCard title="Password" description="Update your secure access key" delay="0.1s">
|
|
<form onSubmit={handlePasswordSubmit} className="space-y-8">
|
|
<div className="max-w-md">
|
|
<InputField label="Current Password" id="current_password" type="password"
|
|
value={passwordData.current_password}
|
|
onChange={(e: any) => setPasswordData('current_password', e.target.value)}
|
|
error={passwordErrors.current_password} required placeholder="••••••••" />
|
|
</div>
|
|
<div className="h-px bg-gray-50 -mx-8" />
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8">
|
|
<InputField label="New Password" id="password" type="password"
|
|
value={passwordData.password}
|
|
onChange={(e: any) => setPasswordData('password', e.target.value)}
|
|
error={passwordErrors.password} required placeholder="Enter new password" />
|
|
<InputField label="Confirm New Password" id="password_confirmation" type="password"
|
|
value={passwordData.password_confirmation}
|
|
onChange={(e: any) => setPasswordData('password_confirmation', e.target.value)}
|
|
error={(passwordErrors as any).password_confirmation} required placeholder="Repeat new password" />
|
|
</div>
|
|
<div className="flex justify-end pt-4 border-t border-gray-50">
|
|
<button type="submit" disabled={passwordProcessing}
|
|
className="h-10 px-8 bg-[#3D4E4B] text-white text-sm font-bold tracking-tight rounded-xl hover:bg-[#2D3A38] transition-all shadow-lg shadow-[#3D4E4B]/20 disabled:opacity-60">
|
|
{passwordProcessing ? 'Updating...' : 'Update Password'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</SectionCard>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === '2fa' && (
|
|
<div className="max-w-4xl space-y-8 anim-fade">
|
|
<div className="p-5 bg-amber-50 border border-amber-200 rounded-2xl flex items-start gap-3">
|
|
<svg className="w-5 h-5 text-amber-500 mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
|
</svg>
|
|
<div>
|
|
<p className="text-xs font-bold text-amber-800 mb-1">Two-Factor Authentication (2FA)</p>
|
|
<p className="text-[11px] text-amber-700 font-medium leading-relaxed">
|
|
2FA menambah lapisan keamanan ekstra dengan meminta kode OTP dari aplikasi authenticator setiap kali login.
|
|
</p>
|
|
</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" />}
|
|
</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>
|
|
))}
|
|
</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>
|
|
<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>
|
|
)}
|
|
|
|
{/* 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.'}
|
|
</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>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'danger' && (
|
|
<div className="max-w-4xl anim-fade">
|
|
<SectionCard title="Account Lifecycle" description="Critical and irreversible account actions" delay="0.1s">
|
|
<div className="flex items-center justify-between gap-10 p-6 rounded-2xl bg-red-50 border border-red-100">
|
|
<div className="flex items-center gap-6">
|
|
<div className="w-14 h-14 rounded-2xl bg-white border border-red-100 flex items-center justify-center text-red-600 shrink-0">
|
|
<svg className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
|
</div>
|
|
<div>
|
|
<p className="text-base font-bold text-red-900 leading-none">Delete Account Permanently</p>
|
|
<p className="text-xs text-red-700/60 font-semibold mt-2 leading-relaxed">
|
|
Proceeding will scrub your identity, logs, and settings from our database. This action is irreversible.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button onClick={handleDeleteAccount}
|
|
className="h-11 px-8 bg-red-600 text-white text-xs font-bold uppercase tracking-widest rounded-xl hover:bg-red-700 transition-all shrink-0">
|
|
Delete Account
|
|
</button>
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
}
|