feat: localize UI to Indonesian, update profile form with extended fields, and disable 2FA password confirmation requirement

This commit is contained in:
Yoga Pangestu 2026-04-24 19:54:59 +07:00
parent 9e973948d2
commit 22f214dc64
12 changed files with 435 additions and 329 deletions

View File

@ -15,8 +15,14 @@ trait ProfileValidationRules
protected function profileRules(?int $userId = null): array
{
return [
'name' => $this->nameRules(),
'username' => $this->usernameRules($userId),
'email' => $this->emailRules($userId),
'nik' => ['required', 'string', 'size:16'],
'full_name' => ['required', 'string', 'max:100'],
'phone_number' => ['required', 'string', 'max:20'],
'address' => ['required', 'string'],
'birth_place' => ['required', 'string', 'max:100'],
'birth_date' => ['required', 'date'],
];
}
@ -25,9 +31,16 @@ protected function profileRules(?int $userId = null): array
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
*/
protected function nameRules(): array
protected function usernameRules(?int $userId = null): array
{
return ['required', 'string', 'max:255'];
return [
'required',
'string',
'max:20',
$userId === null
? Rule::unique(User::class)
: Rule::unique(User::class)->ignore($userId),
];
}
/**

View File

@ -9,6 +9,7 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
@ -30,17 +31,32 @@ public function edit(Request $request): Response
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
$user = $request->user();
$validated = $request->validated();
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
DB::transaction(function () use ($user, $validated) {
$user->fill([
'username' => $validated['username'],
'email' => $validated['email'],
]);
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
$request->user()->save();
$user->save();
Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]);
$user->profile()->update([
'nik' => $validated['nik'],
'full_name' => $validated['full_name'],
'phone_number' => $validated['phone_number'],
'address' => $validated['address'],
'birth_place' => $validated['birth_place'],
'birth_date' => $validated['birth_date'],
]);
});
return to_route('profile.edit');
return redirect()->route('profile.edit')->with('success', __('Profile updated.'));
}
/**

View File

@ -31,7 +31,7 @@ public function user(): BelongsTo
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['full_name', 'phone', 'address', 'base_salary'])
->logOnly(['full_name', 'phone_number', 'address', 'base_salary'])
->logOnlyDirty()
->useLogName('Profil Pegawai');
}
@ -48,7 +48,7 @@ public function tapActivity(Activity $activity, string $eventName)
if (isset($activity->properties['attributes'])) {
$attributeMap = [
'full_name' => 'Nama Lengkap',
'phone' => 'No. Telepon',
'phone_number' => 'No. Telepon',
'address' => 'Alamat',
'base_salary' => 'Gaji Pokok',
];

View File

@ -149,7 +149,7 @@
Features::emailVerification(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
'confirmPassword' => false,
// 'window' => 0
]),
],

View File

@ -58,5 +58,7 @@
"Verify Email Address": "Verifikasi Alamat Surel",
"Verify your email address": "Verifikasi alamat email Anda",
"Whoops!": "Aduh!",
"You are receiving this email because we received a password reset request for your account.": "Anda menerima surel ini karena kami menerima permintaan pengaturan ulang kata sandi untuk akun anda."
"You are receiving this email because we received a password reset request for your account.": "Anda menerima surel ini karena kami menerima permintaan pengaturan ulang kata sandi untuk akun anda.",
"Profile updated.": "Profil berhasil diperbarui.",
"Password updated.": "Kata sandi berhasil diperbarui."
}

View File

@ -12,9 +12,9 @@ export default function AppearanceToggleTab({
const { appearance, updateAppearance } = useAppearance();
const tabs: { value: Appearance; icon: LucideIcon; label: string }[] = [
{ value: 'light', icon: Sun, label: 'Light' },
{ value: 'dark', icon: Moon, label: 'Dark' },
{ value: 'system', icon: Monitor, label: 'System' },
{ value: 'light', icon: Sun, label: 'Terang' },
{ value: 'dark', icon: Moon, label: 'Gelap' },
{ value: 'system', icon: Monitor, label: 'Sistem' },
];
return (

View File

@ -23,14 +23,14 @@ export default function DeleteUser() {
<div className="space-y-6">
<Heading
variant="small"
title="Delete account"
description="Delete your account and all of its resources"
title="Hapus akun"
description="Hapus akun Anda dan semua sumber daya di dalamnya"
/>
<div className="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
<div className="relative space-y-0.5 text-red-600 dark:text-red-100">
<p className="font-medium">Warning</p>
<p className="font-medium">Peringatan</p>
<p className="text-sm">
Please proceed with caution, this cannot be undone.
Harap berhati-hati, tindakan ini tidak dapat dibatalkan.
</p>
</div>
@ -40,18 +40,17 @@ export default function DeleteUser() {
variant="destructive"
data-test="delete-user-button"
>
Delete account
Hapus Akun
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
Are you sure you want to delete your account?
Apakah Anda yakin ingin menghapus akun Anda?
</DialogTitle>
<DialogDescription>
Once your account is deleted, all of its resources
and data will also be permanently deleted. Please
enter your password to confirm you would like to
permanently delete your account.
Setelah akun Anda dihapus, semua sumber daya dan datanya
akan dihapus secara permanen. Silakan masukkan kata sandi Anda
untuk mengonfirmasi bahwa Anda ingin menghapus akun Anda secara permanen.
</DialogDescription>
<Form
@ -70,14 +69,14 @@ export default function DeleteUser() {
htmlFor="password"
className="sr-only"
>
Password
Kata Sandi
</Label>
<PasswordInput
id="password"
name="password"
ref={passwordInput}
placeholder="Password"
placeholder="Kata Sandi"
autoComplete="current-password"
/>
@ -92,7 +91,7 @@ export default function DeleteUser() {
resetAndClearErrors()
}
>
Cancel
Batal
</Button>
</DialogClose>
@ -105,7 +104,7 @@ export default function DeleteUser() {
type="submit"
data-test="confirm-delete-user-button"
>
Delete account
Hapus Akun
</button>
</Button>
</DialogFooter>

View File

@ -41,7 +41,7 @@ export function UserMenuContent({ user }: Props) {
onClick={cleanup}
>
<Settings className="mr-2" />
Settings
Pengaturan
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
@ -55,7 +55,7 @@ export function UserMenuContent({ user }: Props) {
data-test="logout-button"
>
<LogOut className="mr-2" />
Log out
Keluar
</Link>
</DropdownMenuItem>
</>

View File

@ -12,17 +12,17 @@ import type { NavItem } from '@/types';
const sidebarNavItems: NavItem[] = [
{
title: 'Profile',
title: 'Profil',
href: edit(),
icon: null,
},
{
title: 'Security',
title: 'Keamanan',
href: editSecurity(),
icon: null,
},
{
title: 'Appearance',
title: 'Tampilan',
href: editAppearance(),
icon: null,
},
@ -34,15 +34,14 @@ export default function SettingsLayout({ children }: PropsWithChildren) {
return (
<div className="px-4 py-6">
<Heading
title="Settings"
description="Manage your profile and account settings"
title="Pengaturan"
/>
<div className="flex flex-col lg:flex-row lg:space-x-12">
<aside className="w-full max-w-xl lg:w-48">
<nav
className="flex flex-col space-y-1 space-x-0"
aria-label="Settings"
aria-label="Pengaturan"
>
{sidebarNavItems.map((item, index) => (
<Button
@ -67,8 +66,8 @@ export default function SettingsLayout({ children }: PropsWithChildren) {
<Separator className="my-6 lg:hidden" />
<div className="flex-1 md:max-w-2xl">
<section className="max-w-xl space-y-12">
<div className="flex-1">
<section className="space-y-12">
{children}
</section>
</div>

View File

@ -6,15 +6,15 @@ import { edit as editAppearance } from '@/routes/appearance';
export default function Appearance() {
return (
<>
<Head title="Appearance settings" />
<Head title="Pengaturan Tampilan" />
<h1 className="sr-only">Appearance settings</h1>
<h1 className="sr-only">Pengaturan Tampilan</h1>
<div className="space-y-6">
<Heading
variant="small"
title="Appearance settings"
description="Update your account's appearance settings"
title="Tampilan"
description="Sesuaikan tampilan aplikasi sesuai keinginan Anda"
/>
<AppearanceTabs />
</div>
@ -25,7 +25,7 @@ export default function Appearance() {
Appearance.layout = {
breadcrumbs: [
{
title: 'Appearance settings',
title: 'Pengaturan Tampilan',
href: editAppearance(),
},
],

View File

@ -1,133 +1,222 @@
import { Form, Head, Link, usePage } from '@inertiajs/react';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import DeleteUser from '@/components/delete-user';
import Heading from '@/components/heading';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Calendar } from "@/components/ui/calendar";
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldError } from "@/components/ui/field";
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Textarea } from '@/components/ui/textarea';
import { edit } from '@/routes/profile';
import { send } from '@/routes/verification';
import { Head, useForm, usePage } from '@inertiajs/react';
import { Loader, Save } from 'lucide-react';
import React from 'react';
import { toast } from 'sonner';
export default function Profile({
mustVerifyEmail,
status,
}: {
mustVerifyEmail: boolean;
status?: string;
}) {
const { auth } = usePage().props;
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
const { data, setData, patch, processing, errors } = useForm({
nik: auth.user.profile?.nik || '',
full_name: auth.user.profile?.full_name || '',
email: auth.user.email || '',
username: auth.user.username || '',
phone_number: auth.user.profile?.phone_number || '',
birth_place: auth.user.profile?.birth_place || '',
birth_date: auth.user.profile?.birth_date || '',
address: auth.user.profile?.address || '',
});
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
patch(ProfileController.update().url, {
preserveScroll: true,
onSuccess: (response: any) => {
toast.success(response.props.flash.success || 'Profil berhasil diperbarui.');
},
});
};
return (
<>
<Head title="Profile settings" />
<div className="flex flex-col gap-6">
<Head title="Profil" />
<h1 className="sr-only">Profile settings</h1>
<div className="space-y-6">
<Heading
variant="small"
title="Profile information"
description="Update your name and email address"
/>
<Form
{...ProfileController.update.form()}
options={{
preserveScroll: true,
}}
className="space-y-6"
>
{({ processing, errors }) => (
<>
<div className="grid gap-2">
<Label htmlFor="name" required>Name</Label>
<h1 className="sr-only">Profil</h1>
<form onSubmit={onSubmit} className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Informasi Personal</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Field>
<Label htmlFor="nik" required>NIK</Label>
<Input
id="name"
className="mt-1 block w-full"
defaultValue={auth.user.name}
name="name"
required
autoComplete="name"
placeholder="Full name"
id="nik"
value={data.nik}
onChange={(e) => setData('nik', e.target.value)}
placeholder="Contoh: 3213051307900001"
maxLength={16}
autoComplete="off"
/>
<FieldError error={errors.nik} label="NIK" className="text-xs" />
</Field>
<InputError
className="mt-2"
message={errors.name}
label="Name"
<Field>
<Label htmlFor="full_name" required>Nama Lengkap</Label>
<Input
id="full_name"
value={data.full_name}
onChange={(e) => setData('full_name', e.target.value)}
placeholder="Nama Lengkap"
autoComplete="off"
/>
<FieldError error={errors.full_name} label="Nama Lengkap" className="text-xs" />
</Field>
<Field>
<Label htmlFor="phone_number" required>Nomor Telepon</Label>
<Input
id="phone_number"
value={data.phone_number}
onChange={(e) => setData('phone_number', e.target.value)}
placeholder="Nomor Telepon"
autoComplete="off"
/>
<FieldError error={errors.phone_number} label="Nomor Telepon" className="text-xs" />
</Field>
<Field>
<Label htmlFor="birth_place" required>Tempat Lahir</Label>
<Input
id="birth_place"
value={data.birth_place}
onChange={(e) => setData('birth_place', e.target.value)}
placeholder="Tempat Lahir"
autoComplete="off"
/>
<FieldError error={errors.birth_place} label="Tempat Lahir" className="text-xs" />
</Field>
<Field>
<Label htmlFor="birth_date" required>Tanggal Lahir</Label>
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="birth_date"
className="w-full justify-start font-normal"
>
{data.birth_date ? (
new Intl.DateTimeFormat("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
}).format(new Date(data.birth_date))
) : (
<span className="text-muted-foreground">Pilih Tanggal</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={data.birth_date ? new Date(data.birth_date) : undefined}
defaultMonth={data.birth_date ? new Date(data.birth_date) : new Date(2000, 0, 1)}
captionLayout="dropdown"
onSelect={(selectedDate: Date | undefined) => {
if (selectedDate) {
setData('birth_date', selectedDate.getFullYear() + "-" + String(selectedDate.getMonth() + 1).padStart(2, '0') + "-" + String(selectedDate.getDate()).padStart(2, '0'));
} else {
setData('birth_date', '');
}
setIsCalendarOpen(false);
}}
/>
</PopoverContent>
</Popover>
<FieldError error={errors.birth_date} label="Tanggal Lahir" className="text-xs" />
</Field>
</div>
<div className="grid gap-2">
<Label htmlFor="email" required>Email address</Label>
<Field>
<Label htmlFor="address" required>Alamat</Label>
<Textarea
id="address"
value={data.address}
onChange={(e) => setData('address', e.target.value)}
placeholder="Alamat Lengkap"
rows={3}
/>
<FieldError error={errors.address} label="Alamat" className="text-xs" />
</Field>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Kredensial Akun</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<Field>
<Label htmlFor="email" required>Alamat Email</Label>
<Input
id="email"
type="email"
className="mt-1 block w-full"
defaultValue={auth.user.email}
name="email"
required
autoComplete="username"
placeholder="Email address"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
autoComplete="off"
placeholder="Alamat Email"
/>
<FieldError error={errors.email} label="Alamat Email" className="text-xs" />
</Field>
<InputError
className="mt-2"
message={errors.email}
label="Email address"
<Field>
<Label htmlFor="username" required>Nama Pengguna</Label>
<Input
id="username"
value={data.username}
onChange={(e) => setData('username', e.target.value)}
placeholder="Username"
autoComplete="off"
/>
</div>
<FieldError error={errors.username} label="Nama Pengguna" className="text-xs" />
</Field>
</CardContent>
</Card>
{mustVerifyEmail &&
auth.user.email_verified_at === null && (
<div>
<p className="-mt-4 text-sm text-muted-foreground">
Your email address is unverified.{' '}
<Link
href={send()}
as="button"
className="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
>
Click here to resend the
verification email.
</Link>
</p>
{status ===
'verification-link-sent' && (
<div className="mt-2 text-sm font-medium text-green-600">
A new verification link has been
sent to your email address.
</div>
)}
</div>
)}
<div className="flex items-center gap-4">
<Button
disabled={processing}
data-test="update-profile-button"
>
Save
<div className="flex flex-col gap-4 p-4 rounded-xl bg-primary/5 border border-primary/10 shadow-sm">
<Button type="submit" className="w-full" disabled={processing}>
{processing ? <Loader className="size-4 animate-spin" /> : <Save className="size-4" />}
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</div>
</>
)}
</Form>
</div>
<DeleteUser />
</>
</div>
</form>
</div>
);
}
Profile.layout = {
breadcrumbs: [
{
title: 'Profile settings',
title: 'Pengaturan Profil',
href: edit(),
},
],

View File

@ -1,17 +1,18 @@
import { Form, Head } from '@inertiajs/react';
import { ShieldCheck } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import SecurityController from '@/actions/App/Http/Controllers/Settings/SecurityController';
import Heading from '@/components/heading';
import InputError from '@/components/input-error';
import PasswordInput from '@/components/password-input';
import TwoFactorRecoveryCodes from '@/components/two-factor-recovery-codes';
import TwoFactorSetupModal from '@/components/two-factor-setup-modal';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldError } from "@/components/ui/field";
import { Label } from '@/components/ui/label';
import { useTwoFactorAuth } from '@/hooks/use-two-factor-auth';
import { edit } from '@/routes/security';
import { disable, enable } from '@/routes/two-factor';
import { Head, useForm } from '@inertiajs/react';
import { Loader, Save, ShieldCheck } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
type Props = {
canManageTwoFactor?: boolean;
@ -36,11 +37,20 @@ export default function Security({
fetchSetupData,
recoveryCodesList,
fetchRecoveryCodes,
errors,
errors: tfaErrors,
} = useTwoFactorAuth();
const [showSetupModal, setShowSetupModal] = useState<boolean>(false);
const prevTwoFactorEnabled = useRef(twoFactorEnabled);
const { data, setData, put, processing, errors, reset } = useForm({
current_password: '',
password: '',
password_confirmation: '',
});
const { post: postEnable, processing: processingEnable } = useForm({});
const { post: postDisable, processing: processingDisable } = useForm({});
useEffect(() => {
if (prevTwoFactorEnabled.current && !twoFactorEnabled) {
clearTwoFactorAuthData();
@ -49,180 +59,159 @@ export default function Security({
prevTwoFactorEnabled.current = twoFactorEnabled;
}, [twoFactorEnabled, clearTwoFactorAuthData]);
return (
<>
<Head title="Security settings" />
<h1 className="sr-only">Security settings</h1>
<div className="space-y-6">
<Heading
variant="small"
title="Update password"
description="Ensure your account is using a long, random password to stay secure"
/>
<Form
{...SecurityController.update.form()}
options={{
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
put(SecurityController.update().url, {
preserveScroll: true,
}}
resetOnError={[
'password',
'password_confirmation',
'current_password',
]}
resetOnSuccess
onError={(errors) => {
onSuccess: () => {
toast.success('Kata sandi berhasil diperbarui.');
reset();
},
onError: (errors) => {
if (errors.password) {
passwordInput.current?.focus();
}
if (errors.current_password) {
currentPasswordInput.current?.focus();
}
}}
className="space-y-6"
>
{({ errors, processing }) => (
<>
<div className="grid gap-2">
<Label htmlFor="current_password" required>
Current password
</Label>
},
});
};
const handleEnable2FA = (e: React.FormEvent) => {
e.preventDefault();
postEnable(enable().url, {
onSuccess: () => setShowSetupModal(true),
});
};
const handleDisable2FA = (e: React.FormEvent) => {
e.preventDefault();
postDisable(disable().url);
};
return (
<div className="flex flex-col gap-6">
<Head title="Keamanan Akun" />
<h1 className="sr-only">Keamanan Akun</h1>
<form onSubmit={onSubmit} className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Perbarui Kata Sandi</CardTitle>
</CardHeader>
<CardContent className="space-y-6 pt-6">
<Field>
<Label htmlFor="current_password" required>Kata Sandi Saat Ini</Label>
<PasswordInput
id="current_password"
ref={currentPasswordInput}
name="current_password"
className="mt-1 block w-full"
value={data.current_password}
onChange={(e) => setData('current_password', e.target.value)}
className="w-full"
autoComplete="current-password"
placeholder="Current password"
placeholder="Masukkan kata sandi saat ini"
/>
<FieldError error={errors.current_password} label="Kata sandi saat ini" className="text-xs" />
</Field>
<InputError message={errors.current_password} label="Current password" />
</div>
<div className="grid gap-2">
<Label htmlFor="password" required>New password</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Field>
<Label htmlFor="password" required>Kata Sandi Baru</Label>
<PasswordInput
id="password"
ref={passwordInput}
name="password"
className="mt-1 block w-full"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
placeholder="Masukkan kata sandi baru"
autoComplete="new-password"
placeholder="New password"
/>
<FieldError error={errors.password} label="Kata sandi baru" className="text-xs" />
</Field>
<InputError message={errors.password} label="New password" />
</div>
<div className="grid gap-2">
<Label htmlFor="password_confirmation" required>
Confirm password
</Label>
<Field>
<Label htmlFor="password_confirmation" required>Konfirmasi Kata Sandi Baru</Label>
<PasswordInput
id="password_confirmation"
name="password_confirmation"
className="mt-1 block w-full"
value={data.password_confirmation}
onChange={(e) => setData('password_confirmation', e.target.value)}
placeholder="Ulangi kata sandi baru"
autoComplete="new-password"
placeholder="Confirm password"
/>
<InputError
message={errors.password_confirmation}
label="Confirm password"
/>
<FieldError error={errors.password_confirmation} label="Konfirmasi kata sandi baru" className="text-xs" />
</Field>
</div>
<div className="flex items-center gap-4">
<Button
disabled={processing}
data-test="update-password-button"
>
Save password
<div className="flex justify-end">
<Button type="submit" disabled={processing}>
{processing ? <Loader className="size-4 animate-spin" /> : <Save className="size-4" />}
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</div>
</>
)}
</Form>
</div>
</CardContent>
</Card>
{canManageTwoFactor && (
<div className="space-y-6">
<Heading
variant="small"
title="Two-factor authentication"
description="Manage your two-factor authentication settings"
/>
{twoFactorEnabled ? (
<div className="flex flex-col items-start justify-start space-y-4">
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Autentikasi Dua Faktor (2FA)</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
You will be prompted for a secure, random pin
during login, which you can retrieve from the
TOTP-supported application on your phone.
{twoFactorEnabled
? "Anda akan diminta memasukkan PIN acak yang aman saat masuk, yang dapat Anda ambil dari aplikasi pendukung TOTP di ponsel Anda."
: "Saat Anda mengaktifkan autentikasi dua faktor, Anda akan diminta PIN aman saat masuk. PIN ini dapat diambil dari aplikasi pendukung TOTP di ponsel Anda."
}
</p>
<div className="relative inline">
<Form {...disable.form()}>
{({ processing }) => (
{twoFactorEnabled ? (
<div className="space-y-4">
<Button
type="button"
variant="destructive"
type="submit"
disabled={processing}
onClick={handleDisable2FA}
disabled={processingDisable}
>
Disable 2FA
{processingDisable ? <Loader className="size-4 animate-spin" /> : null}
Nonaktifkan 2FA
</Button>
)}
</Form>
</div>
<TwoFactorRecoveryCodes
recoveryCodesList={recoveryCodesList}
fetchRecoveryCodes={fetchRecoveryCodes}
errors={errors}
errors={tfaErrors}
/>
</div>
) : (
<div className="flex flex-col items-start justify-start space-y-4">
<p className="text-sm text-muted-foreground">
When you enable two-factor authentication, you
will be prompted for a secure pin during login.
This pin can be retrieved from a TOTP-supported
application on your phone.
</p>
<div>
{hasSetupData ? (
<Button
onClick={() => setShowSetupModal(true)}
>
<ShieldCheck />
Continue setup
<Button type="button" onClick={() => setShowSetupModal(true)}>
<ShieldCheck className="mr-2 h-4 w-4" />
Lanjutkan Penyiapan
</Button>
) : (
<Form
{...enable.form()}
onSuccess={() =>
setShowSetupModal(true)
}
>
{({ processing }) => (
<Button
type="submit"
disabled={processing}
type="button"
onClick={handleEnable2FA}
disabled={processingEnable}
>
Enable 2FA
{processingEnable ? <Loader className="size-4 animate-spin" /> : null}
Aktifkan 2FA
</Button>
)}
</Form>
</div>
)}
</CardContent>
</Card>
)}
</div>
</div>
)}
</form>
{canManageTwoFactor && (
<TwoFactorSetupModal
isOpen={showSetupModal}
onClose={() => setShowSetupModal(false)}
@ -232,18 +221,17 @@ export default function Security({
manualSetupKey={manualSetupKey}
clearSetupData={clearSetupData}
fetchSetupData={fetchSetupData}
errors={errors}
errors={tfaErrors}
/>
</div>
)}
</>
</div>
);
}
Security.layout = {
breadcrumbs: [
{
title: 'Security settings',
title: 'Keamanan Akun',
href: edit(),
},
],