Compare commits

...

3 Commits

Author SHA1 Message Date
a291bdfb51 Tampilan Layout sudah sesuai dengan roleIdnya
Sudah sessionnya
Sudah disesuaikan juga semisal superadmin ngubah role admin, maka admin tersebut akan logOut dan diarahkan ke halama login
sudah bisa logOut
2025-11-21 17:26:38 +08:00
0dff8f3254 Nico 20 Nov 25
Dibagian layout admin sudah disesuaikan dengan rolenya : supadmin, admin desa, admin kesehatan, admin pendidikan
Fix API User & Role Admin
2025-11-20 16:42:36 +08:00
78b8aa74cd Saat user baru registrasi maka akan diarahkan ke page waiting-room dan menunggu validasi admin 2025-11-20 14:07:26 +08:00
29 changed files with 1806 additions and 540 deletions

View File

@@ -1,23 +1,26 @@
[
{
"id": "role-1",
"name": "ADMIN DESA",
"description": "Administrator Desa",
"permissions": ["manage_users", "manage_content", "view_reports"],
"isActive": true
},
{
"id": "role-2",
"name": "ADMIN KESEHATAN",
"description": "Administrator Bidang Kesehatan",
"permissions": ["manage_health_data", "view_reports"],
"isActive": true
},
{
"id": "role-3",
"name": "ADMIN SEKOLAH",
"description": "Administrator Sekolah",
"permissions": ["manage_school_data", "view_reports"],
"isActive": true
}
]
{
"id": "0",
"name": "SUPER ADMIN",
"description": "Administrator",
"isActive": true
},
{
"id": "1",
"name": "ADMIN DESA",
"description": "Administrator Desa",
"isActive": true
},
{
"id": "2",
"name": "ADMIN KESEHATAN",
"description": "Administrator Bidang Kesehatan",
"isActive": true
},
{
"id": "3",
"name": "ADMIN PENDIDIKAN",
"description": "Administrator Bidang Pendidikan",
"isActive": true
}
]

View File

@@ -1,23 +0,0 @@
[
{
"id": "user-1",
"nama": "Admin Desa",
"nomor": "089647037426",
"roleId": "role-1",
"isActive": true
},
{
"id": "user-2",
"nama": "Admin Kesehatan",
"nomor": "082339004198",
"roleId": "role-2",
"isActive": true
},
{
"id": "user-3",
"nama": "Admin Sekolah",
"nomor": "085237157222",
"roleId": "role-3",
"isActive": true
}
]

View File

@@ -2181,7 +2181,6 @@ model Role {
id String @id @default(cuid())
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
description String?
permissions Json // Menyimpan permission dalam format JSON
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -2200,17 +2199,6 @@ model KodeOtp {
otp Int
}
// Tabel untuk menyimpan permission
model Permission {
id String @id @default(cuid())
name String @unique
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("permissions")
}
model UserSession {
id String @id @default(cuid())
token String

View File

@@ -54,14 +54,13 @@ import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-progr
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import roles from "./data/user/roles.json";
import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
import seedAssets from "./seed_assets";
import { safeSeedUnique } from "./safeseedUnique";
(async () => {
// =========== USER & ROLE ===========
// =========== ROLE ===========
// In your seed.ts
// =========== ROLES ===========
console.log("🔄 Seeding roles...");
@@ -69,35 +68,12 @@ import { safeSeedUnique } from "./safeseedUnique";
await safeSeedUnique("role", { id: r.id }, {
name: r.name,
description: r.description,
permissions: r.permissions,
isActive: r.isActive,
});
}
console.log("✅ Roles seeded");
// =========== USERS ===========
console.log("🔄 Seeding users...");
for (const u of users) {
// First verify the role exists
const roleExists = await prisma.role.findUnique({
where: { id: u.roleId },
});
if (!roleExists) {
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
continue;
}
await safeSeedUnique("user", { id: u.id }, {
username: u.nama,
nomor: u.nomor,
roleId: u.roleId,
isActive: u.isActive,
});
}
console.log("✅ Users seeded");
// =========== FILE STORAGE ===========
console.log("🔄 Seeding file storage...");
for (const f of fileStorage) {

View File

@@ -90,42 +90,96 @@ const userState = proxy({
}
},
},
updateActive: {
deleteUser: {
loading: false,
async submit(id: string, isActive: boolean) {
this.loading = true;
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
const res = await fetch(`/api/user/updt`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, isActive }),
userState.deleteUser.loading = true;
const response = await fetch(`/api/user/delUser/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const data = await res.json();
if (res.status === 200 && data.success) {
toast.success(data.message);
userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "User berhasil dihapus permanen");
await userState.findMany.load(); // refresh list user setelah delete
} else {
toast.error(data.message || "Gagal update status user");
toast.error(result?.message || "Gagal menghapus user");
}
} catch (e) {
console.error(e);
toast.error("Gagal update status user");
} catch (error) {
console.error("Gagal delete user:", error);
toast.error("Terjadi kesalahan saat menghapus user");
} finally {
this.loading = false;
userState.delete.loading = false;
}
},
},
// Di file userState.ts atau dimana state user berada
update: {
loading: false,
async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
this.loading = true;
try {
const res = await fetch(`/api/user/updt`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (res.status === 200 && data.success) {
// ✅ Tampilkan pesan yang berbeda jika role berubah
if (data.roleChanged) {
toast.success(
`${data.message}\n\nUser akan logout otomatis dalam beberapa detik.`,
{
autoClose: 5000,
}
);
} else {
toast.success(data.message);
}
// Refresh list
await userState.findMany.load(
userState.findMany.page,
10,
userState.findMany.search
);
return true; // ✅ Return success untuk handling di component
} else {
toast.error(data.message || "Gagal update user");
return false;
}
} catch (e) {
console.error("❌ Error update user:", e);
toast.error("Gagal update user");
return false;
} finally {
this.loading = false;
}
},
},
});
const templateRole = z.object({
name: z.string().min(1, "Nama harus diisi"),
permissions: z.array(z.string()).min(1, "Permission harus diisi"),
});
const defaultRole = {
name: "",
permissions: [] as string[],
};
const roleState = proxy({
@@ -237,7 +291,7 @@ const roleState = proxy({
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/role/${id}`, {
method: "GET",
@@ -245,31 +299,25 @@ const roleState = proxy({
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
// langsung set melalui root state, bukan this
roleState.update.id = data.id;
roleState.update.form = {
name: data.name,
permissions: data.permissions,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
return data;
}
} catch (error) {
console.error("Error loading role:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
toast.error("Gagal memuat data");
}
},
},
async update() {
const cek = templateRole.safeParse(roleState.update.form);
if (!cek.success) {
@@ -290,7 +338,6 @@ const roleState = proxy({
},
body: JSON.stringify({
name: this.form.name,
permissions: this.form.permissions,
}),
});

View File

@@ -1,4 +1,5 @@
// app/validasi/page.tsx
//
'use client';
import { apiFetchOtpData, apiFetchVerifyOtp } from '@/app/api/auth/_lib/api_fetch_auth';
@@ -7,6 +8,7 @@ import { Box, Button, Loader, Paper, PinInput, Stack, Text, Title } from '@manti
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { authStore } from '@/store/authStore';
export default function Validasi() {
const router = useRouter();
@@ -16,82 +18,151 @@ export default function Validasi() {
const [isLoading, setIsLoading] = useState(true);
const [kodeId, setKodeId] = useState<string | null>(null);
// Inisialisasi data OTP
useEffect(() => {
const storedKodeId = localStorage.getItem('auth_kodeId');
if (!storedKodeId) {
toast.error('Akses tidak valid');
router.push('/login');
router.replace('/login');
return;
}
setKodeId(storedKodeId);
const fetchOtpData = async () => {
const loadOtpData = async () => {
try {
const result = await apiFetchOtpData({ kodeId: storedKodeId });
if (result.success && result.data?.nomor) {
setNomor(result.data.nomor);
} else {
throw new Error('OTP tidak valid');
throw new Error('Data OTP tidak valid');
}
} catch (error) {
console.error('Gagal muat OTP:', error);
console.error('Gagal memuat data OTP:', error);
toast.error('Kode verifikasi tidak valid');
router.push('/login');
router.replace('/login');
} finally {
setIsLoading(false);
}
};
fetchOtpData();
loadOtpData();
}, [router]);
// Verifikasi OTP
const handleVerify = async () => {
if (!kodeId || !nomor || otp.length < 4) return;
setLoading(true);
try {
setLoading(true);
const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId });
if (verifyResult.success) {
cleanupStorage();
router.push('/admin/landing-page/profil/program-inovasi');
return; // ✅ HENTIKAN eksekusi di sini
}
// Hanya coba registrasi jika akun tidak ditemukan
if (verifyResult.status === 404 && verifyResult.message?.includes('Akun tidak ditemukan')) {
const username = localStorage.getItem('auth_username');
if (!username) {
toast.error('Data registrasi hilang');
if (!verifyResult.success) {
// Registrasi baru?
if (
verifyResult.status === 404 &&
verifyResult.message?.includes('Akun tidak ditemukan')
) {
await handleNewRegistration();
return;
}
const regRes = await fetch('/api/auth/finalize-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nomor, username, otp, kodeId }),
});
const regData = await regRes.json();
if (regData.success) {
cleanupStorage();
router.push('/admin/landing-page/profil/program-inovasi');
} else {
toast.error(regData.message || 'Registrasi gagal');
}
} else {
// Hanya tampilkan error jika bukan kasus "akun tidak ditemukan"
// Error lain
toast.error(verifyResult.message || 'Verifikasi gagal');
return;
}
// ✅ Verifikasi sukses → simpan user ke store
const user = verifyResult.user;
console.log('=== DEBUG USER ===');
console.log('Full user object:', user);
if (!user || !user.id) {
toast.error('Data pengguna tidak lengkap');
return;
}
const roleId = Number(user.roleId);
authStore.setUser({
id: user.id,
name: user.name || user.username || 'User',
roleId: roleId,
});
cleanupStorage();
const isUserActive = user.isActive ?? user.is_active ?? true;
// Redirect berdasarkan status approval
if (!isUserActive) {
router.replace('/waiting-room');
return;
}
// ✅ Switch statement lebih clean
let redirectPath: string;
switch (roleId) {
case 0:
case 1:
redirectPath = '/admin/landing-page/profil/program-inovasi';
break;
case 2:
redirectPath = '/admin/kesehatan/posyandu';
break;
case 3:
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
break;
default:
redirectPath = '/admin';
console.warn('Unknown roleId:', roleId);
}
console.log('Redirecting to:', redirectPath);
router.replace(redirectPath);
} catch (error) {
console.error('Verifikasi error:', error);
toast.error('Terjadi kesalahan');
console.error('Error saat verifikasi:', error);
toast.error('Terjadi kesalahan sistem');
} finally {
setLoading(false);
}
};
// Registrasi baru
const handleNewRegistration = async () => {
const username = localStorage.getItem('auth_username');
if (!username) {
toast.error('Data registrasi tidak ditemukan');
return;
}
try {
const res = await fetch('/api/auth/finalize-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nomor, username, otp, kodeId }),
});
const data = await res.json();
if (data.success) {
// Set user sementara (tanpa roleId, akan diisi saat approve)
authStore.setUser({
id: 'pending',
name: username,
});
cleanupStorage();
router.replace('/waiting-room');
} else {
toast.error(data.message || 'Registrasi gagal');
}
} catch (error) {
console.error('Error registrasi:', error);
toast.error('Gagal menyelesaikan registrasi');
}
};
const cleanupStorage = () => {
localStorage.removeItem('auth_kodeId');
localStorage.removeItem('auth_nomor');
@@ -110,12 +181,15 @@ export default function Validasi() {
if (data.success) {
localStorage.setItem('auth_kodeId', data.kodeId);
toast.success('OTP baru dikirim');
} else {
toast.error(data.message || 'Gagal mengirim ulang OTP');
}
} catch {
toast.error('Gagal kirim ulang');
toast.error('Gagal menghubungi server');
}
};
// Loading
if (isLoading) {
return (
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
@@ -140,7 +214,7 @@ export default function Validasi() {
Kami telah mengirim kode ke nomor <strong>{nomor}</strong>
</Text>
</Box>
<Box>
<Box w="100%">
<Box mb={20}>
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
Masukkan Kode Verifikasi
@@ -168,7 +242,14 @@ export default function Validasi() {
<Text ta="center" size="sm" mt="md">
Tidak menerima kode?{' '}
<Button variant="subtle" onClick={handleResend} size="xs" p={0} h="auto" color={colors['blue-button']}>
<Button
variant="subtle"
onClick={handleResend}
size="xs"
p={0}
h="auto"
color={colors['blue-button']}
>
Kirim Ulang
</Button>
</Text>

View File

@@ -1,7 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Loader, Group, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
@@ -9,6 +10,7 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import user from '../../../_state/user/user-state';
function EditRole() {
const stateRole = useProxy(user.roleState);
const router = useRouter();
@@ -17,46 +19,37 @@ function EditRole() {
// Controlled local state
const [formData, setFormData] = useState({
name: '',
permissions: [] as string[],
});
const [originalData, setOriginalData] = useState({
name: '',
permissions: [] as string[],
});
const [isSubmitting, setIsSubmitting] = useState(false);
// Load role data
const loadRole = useCallback(async (id: string) => {
try {
const data = await stateRole.update.load(id);
if (data) {
setFormData({
name: data.name || '',
permissions: data.permissions || [],
});
setOriginalData({
name: data.name || '',
permissions: data.permissions || [],
});
}
} catch (error) {
console.error('Error loading role:', error);
toast.error(error instanceof Error ? error.message : 'Gagal mengambil data role');
const data = await stateRole.update.load(id);
if (data) {
setFormData({
name: data.name ?? '',
});
setOriginalData({
name: data.name ?? '',
});
}
}, [stateRole.update]);
}, []);
useEffect(() => {
stateRole.findMany.load(); // Load permissions/options
const id = params?.id as string;
if (id) loadRole(id);
}, [params?.id, loadRole, stateRole.findMany]);
stateRole.findMany.load(); // load permission
if (params?.id) loadRole(params.id as string);
}, [params?.id]);
const handleResetForm = () => {
setFormData({
name: originalData.name,
permissions: originalData.permissions,
});
toast.info("Form dikembalikan ke data awal");
};
@@ -66,10 +59,6 @@ function EditRole() {
toast.error('Nama role tidak boleh kosong');
return;
}
if (!formData.permissions.length) {
toast.error('Pilih minimal satu permission');
return;
}
try {
setIsSubmitting(true);
@@ -77,7 +66,6 @@ function EditRole() {
stateRole.update.form = {
...stateRole.update.form,
name: formData.name,
permissions: formData.permissions,
};
await stateRole.update.update();
toast.success('Role berhasil diperbarui!');
@@ -116,24 +104,7 @@ function EditRole() {
label={<Text fw="bold" fz="sm">Nama Role</Text>}
placeholder="Masukkan nama role"
/>
<MultiSelect
value={formData.permissions}
onChange={(val) => setFormData({ ...formData, permissions: val })}
label={<Text fw="bold" fz="sm">Permission</Text>}
placeholder="Pilih permission"
data={
stateRole.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
clearable
searchable
required
error={!formData.permissions.length ? 'Pilih minimal satu permission' : undefined}
/>
<Group justify="right">
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"

View File

@@ -5,9 +5,8 @@ import {
Box,
Button,
Group,
MultiSelect,
Paper,
Loader,
Paper,
Stack,
TextInput,
Title
@@ -15,9 +14,9 @@ import {
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import user from '../../../_state/user/user-state';
import { toast } from 'react-toastify';
export default function CreateRole() {
@@ -31,8 +30,7 @@ export default function CreateRole() {
const resetForm = () => {
stateRole.create.form = {
name: '',
permissions: [],
name: ''
};
};
@@ -80,28 +78,6 @@ export default function CreateRole() {
required
/>
</Box>
<Box>
<MultiSelect
label="Permission"
placeholder="Pilih permission"
data={
Array.from(
new Set(
stateRole.findMany.data
?.map((item) => item.permissions)
.flat()
)
)
.filter((p): p is string => typeof p === 'string')
.map((p) => ({ label: p, value: p }))
}
value={stateRole.create.form.permissions}
onChange={(value) => (stateRole.create.form.permissions = value)}
required
/>
</Box>
<Group justify="right">
<Button

View File

@@ -1,15 +1,14 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { Box, Button, Center, Group, Pagination, Paper, Select, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCheck, IconSearch, IconX } from '@tabler/icons-react';
import { IconCheck, IconSearch, IconTrash, IconX } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import user from '../../_state/user/user-state';
function User() {
const [search, setSearch] = useState("");
return (
@@ -27,9 +26,10 @@ function User() {
}
function ListUser({ search }: { search: string }) {
const stateUser = useProxy(user.userState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const stateUser = useProxy(user.userState);
const stateRole = useProxy(user.roleState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
@@ -41,20 +41,80 @@ function ListUser({ search }: { search: string }) {
const handleDelete = () => {
if (selectedId) {
stateUser.delete.submit(selectedId)
setModalHapus(false)
setSelectedId(null)
stateUser.findMany.load()
stateUser.deleteUser.delete(selectedId);
setModalHapus(false);
setSelectedId(null);
stateUser.findMany.load();
}
}
};
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
stateRole.findMany.load();
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
// ✅ Helper function untuk nama role
const getRoleName = (roleId: string) => {
// Cari dari data role yang sudah diload
const role = stateRole.findMany.data.find((r) => r.id === roleId);
return role?.name || "Unknown Role";
};
// ✅ Handler untuk perubahan role dengan konfirmasi
const handleRoleChange = async (
userId: string,
username: string,
oldRoleId: string,
newRoleId: string
) => {
// Skip jika sama
if (oldRoleId === newRoleId) {
return true;
}
// ✅ Konfirmasi perubahan role
const confirmed = window.confirm(
`⚠️ PERINGATAN\n\n` +
`Mengubah role untuk "${username}" akan:\n` +
`• Logout user otomatis dari semua device\n` +
`• Mengubah akses menu sesuai role baru\n\n` +
`Role: ${getRoleName(oldRoleId)}${getRoleName(newRoleId)}\n\n` +
`Lanjutkan?`
);
if (!confirmed) {
// Reload data untuk reset dropdown ke nilai lama
stateUser.findMany.load(page, 10, search);
return false;
}
// ✅ Submit update
const success = await stateUser.update.submit({
id: userId,
roleId: newRoleId,
});
if (success) {
// Reload data setelah berhasil update
stateUser.findMany.load(page, 10, search);
}
return success;
};
// ✅ Handler untuk toggle isActive
const handleToggleActive = async (userId: string, currentStatus: boolean) => {
const success = await stateUser.update.submit({
id: userId,
isActive: !currentStatus,
});
if (success) {
stateUser.findMany.load(page, 10, search);
}
};
const filteredData = data || [];
if (loading || !data) {
return (
@@ -78,25 +138,49 @@ function ListUser({ search }: { search: string }) {
<TableTh style={{ width: '20%' }}>Nomor</TableTh>
<TableTh style={{ width: '20%' }}>Role</TableTh>
<TableTh style={{ width: '15%' }}>Aktif / Nonaktif</TableTh>
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%', }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.username}</Text>
<TableTd style={{ width: '25%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.username}
</Text>
</TableTd>
<TableTd style={{ width: '20%', }}>
<TableTd style={{ width: '20%' }}>
<Text truncate fz="sm" c="dimmed">
{item.nomor}
</Text>
</TableTd>
<TableTd style={{ width: '20%', }}>
<Text truncate fz="sm" c="dimmed">
{item.role.name}
</Text>
<TableTd style={{ width: '20%' }}>
<Select
placeholder="Pilih role"
data={stateRole.findMany.data.map((r) => ({
label: r.name,
value: r.id,
}))}
value={item.roleId}
onChange={(val) => {
if (!val) return;
// ✅ Panggil handleRoleChange dengan konfirmasi
handleRoleChange(
item.id,
item.username,
item.roleId,
val
);
}}
searchable
clearable={false}
nothingFoundMessage="Role tidak ditemukan"
disabled={stateUser.update.loading}
/>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Tooltip
label={item.isActive ? "Nonaktifkan user" : "Aktifkan user"}
@@ -105,22 +189,34 @@ function ListUser({ search }: { search: string }) {
<Button
variant="light"
color={item.isActive ? "green" : "red"}
onClick={async () => {
await stateUser.updateActive.submit(item.id, !item.isActive)
stateUser.findMany.load(page, 10, search)
}}
onClick={() => handleToggleActive(item.id, item.isActive)}
disabled={stateUser.update.loading}
>
{item.isActive ? <IconCheck size={20} /> : <IconX size={20} />}
</Button>
</Tooltip>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"
color='red'
disabled={stateUser.deleteUser.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">Tidak ada data user yang cocok</Text>
<Text c="dimmed">Tidak ada data user yang cocok</Text>
</Center>
</TableTd>
</TableTr>
@@ -129,6 +225,7 @@ function ListUser({ search }: { search: string }) {
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
@@ -143,6 +240,7 @@ function ListUser({ search }: { search: string }) {
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
@@ -154,4 +252,4 @@ function ListUser({ search }: { search: string }) {
);
}
export default User;
export default User;

View File

@@ -84,7 +84,6 @@ export const navBar = [
]
},
{
id: "Desa",
name: "Desa",
@@ -336,7 +335,415 @@ export const navBar = [
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
}
]
}, {
},
{
id: "Pendidikan",
name: "Pendidikan",
path: "",
children: [
{
id: "Pendidikan_1",
name: "Info Sekolah",
path: "/admin/pendidikan/info-sekolah/jenjang-pendidikan"
},
{
id: "Pendidikan_2",
name: "Beasiswa Desa",
path: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar"
},
{
id: "Pendidikan_3",
name: "Program Pendidikan Anak",
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan"
},
{
id: "Pendidikan_4",
name: "Bimbingan Belajar Desa",
path: "/admin/pendidikan/bimbingan-belajar-desa/tujuan-program"
},
{
id: "Pendidikan_5",
name: "Pendidikan Non Formal",
path: "/admin/pendidikan/pendidikan-non-formal/tujuan-program"
},
{
id: "Pendidikan_6",
name: "Perpustakaan Digital",
path: "/admin/pendidikan/perpustakaan-digital/data-perpustakaan"
},
{
id: "Pendidikan_7",
name: "Data Pendidikan",
path: "/admin/pendidikan/data-pendidikan"
}
]
},
{
id: "User & Role",
name: "User & Role",
path: "",
children: [
{
id: "User",
name: "User",
path: "/admin/user&role/user"
},
{
id: "Role",
name: "Role",
path: "/admin/user&role/role"
}
]
}
]
export const role1 = [
{
id: "Landing Page",
name: "Landing Page",
path: "",
children: [
{
id: "Landing_Page_1",
name: "Profil",
path: "/admin/landing-page/profil/program-inovasi"
},
{
id: "Landing_Page_2",
name: "Desa Anti Korupsi",
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
},
{
id: "Landing_Page_3",
name: "Indeks Kepuasan Masyarakat",
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
},
{
id: "Landing_Page_4",
name: "SDGs",
path: "/admin/landing-page/SDGs"
},
{
id: "Landing_Page_5",
name: "APBDes",
path: "/admin/landing-page/apbdes"
},
{
id: "Landing_Page_6",
name: "Prestasi Desa",
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
}
]
},
{
id: "PPID",
name: "PPID",
path: "",
children: [
{
id: "PPID_1",
name: "Profil PPID",
path: "/admin/ppid/profil-ppid"
},
{
id: "PPID_2",
name: "Struktur PPID",
path: "/admin/ppid/struktur-ppid/pegawai"
},
{
id: "PPID_3",
name: "Visi Misi PPID",
path: "/admin/ppid/visi-misi-ppid"
},
{
id: "PPID_4",
name: "Dasar Hukum",
path: "/admin/ppid/dasar-hukum"
},
{
id: "PPID_5",
name: "Permohonan Informasi Publik",
path: "/admin/ppid/permohonan-informasi-publik"
},
{
id: "PPID_6",
name: "Permohonan Keberatan Informasi Publik",
path: "/admin/ppid/permohonan-keberatan-informasi-publik"
},
{
id: "PPID_7",
name: "Daftar Informasi Publik",
path: "/admin/ppid/daftar-informasi-publik"
},
{
id: "PPID_8",
name: "Indeks Kepuasan Masyarakat",
path: "/admin/ppid/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
},
]
},
{
id: "Desa",
name: "Desa",
path: "",
children: [
{
id: "Desa_1",
name: "Profile",
path: "/admin/desa/profile/profile-desa"
},
{
id: "Desa_2",
name: "Potensi",
path: "/admin/desa/potensi/list-potensi"
},
{
id: "Desa_3",
name: "Berita",
path: "/admin/desa/berita/list-berita"
},
{
id: "Desa_4",
name: "Pengumuman",
path: "/admin/desa/pengumuman/list-pengumuman"
},
{
id: "Desa_5",
name: "Gallery",
path: "/admin/desa/gallery/foto"
},
{
id: "Desa_6",
name: "Layanan",
path: "/admin/desa/layanan/pelayanan_surat_keterangan"
},
{
id: "Desa_7",
name: "Penghargaan",
path: "/admin/desa/penghargaan"
}
]
},
{
id: "Keamanan",
name: "Keamanan",
path: "",
children: [
{
id: "Keamanan_1",
name: "Keamanan Lingkungan (Pecalang/Patwal)",
path: "/admin/keamanan/keamanan-lingkungan-pecalang-patwal"
},
{
id: "Keamanan_2",
name: "Polsek Terdekat",
path: "/admin/keamanan/polsek-terdekat"
},
{
id: "Keamanan_3",
name: "Kontak Darurat",
path: "/admin/keamanan/kontak-darurat/kontak-darurat-keamanan"
},
{
id: "Keamanan_4",
name: "Pencegahan Kriminalitas",
path: "/admin/keamanan/pencegahan-kriminalitas"
},
{
id: "Keamanan_5",
name: "Laporan Publik",
path: "/admin/keamanan/laporan-publik"
},
{
id: "Keamanan_6",
name: "Tips Keamanan",
path: "/admin/keamanan/tips-keamanan"
}
]
},
{
id: "Ekonomi",
name: "Ekonomi",
path: "",
children: [
{
id: "Ekonomi_1",
name: "Pasar Desa",
path: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
},
{
id: "Ekonomi_2",
name: "Lowongan Kerja Lokal",
path: "/admin/ekonomi/lowongan-kerja-lokal"
},
{
id: "Ekonomi_3",
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa",
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"
},
{
id: "Ekonomi_4",
name: "PADesa (Pendapatan Asli Desa)",
path: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"
},
{
id: "Ekonomi_5",
name: "Jumlah Pengangguran",
path: "/admin/ekonomi/jumlah-pengangguran"
},
{
id: "Ekonomi_6",
name: "Jumlah penduduk usia kerja yang menganggur",
path: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia"
},
{
id: "Ekonomi_7",
name: "Jumlah Penduduk Miskin",
path: "/admin/ekonomi/jumlah-penduduk-miskin"
},
{
id: "Ekonomi_8",
name: "Program Kemiskinan",
path: "/admin/ekonomi/program-kemiskinan"
},
{
id: "Ekonomi_9",
name: "Sektor Unggulan Desa",
path: "/admin/ekonomi/sektor-unggulan-desa"
},
{
id: "Ekonomi_10",
name: "Demografi Pekerjaan",
path: "/admin/ekonomi/demografi-pekerjaan"
}
]
},
{
id: "Inovasi",
name: "Inovasi",
path: "",
children: [
{
id: "Inovasi_1",
name: "Desa Digital/Smart Village",
path: "/admin/inovasi/desa-digital-smart-village"
},
{
id: "Inovasi_2",
name: "Layanan Online Desa",
path: "/admin/inovasi/layanan-online-desa/administrasi-online"
},
{
id: "Inovasi_3",
name: "Program Kreatif Desa",
path: "/admin/inovasi/program-kreatif-desa"
},
{
id: "Inovasi_4",
name: "Kolaborasi Inovasi",
path: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
},
{
id: "Inovasi_5",
name: "Info Teknologi Tepat Guna",
path: "/admin/inovasi/info-teknologi-tepat-guna"
},
{
id: "Inovasi_6",
name: "Ajukan Ide Inovatif",
path: "/admin/inovasi/ajukan-ide-inovatif"
}
]
},
{
id: "Lingkungan",
name: "Lingkungan",
path: "",
children: [
{
id: "Lingkungan_1",
name: "Pengelolaan Sampah (Bank Sampah)",
path: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
},
{
id: "Lingkungan_2",
name: "Program Penghijauan",
path: "/admin/lingkungan/program-penghijauan"
},
{
id: "Lingkungan_3",
name: "Data Lingkungan Desa",
path: "/admin/lingkungan/data-lingkungan-desa"
},
{
id: "Lingkungan_4",
name: "Gotong Royong",
path: "/admin/lingkungan/gotong-royong/kegiatan-desa"
},
{
id: "Lingkungan_5",
name: "Edukasi Lingkungan",
path: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan"
},
{
id: "Lingkungan_6",
name: "Konservasi Adat Bali",
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
}
]
}
]
export const role2 = [
{
id: "Kesehatan",
name: "Kesehatan",
path: "",
children: [
{
id: "Kesehatan_1",
name: "Posyandu",
path: "/admin/kesehatan/posyandu"
},
{
id: "Kesehatan_2",
name: "Data Kesehatan Warga",
path: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian"
},
{
id: "Kesehatan_3",
name: "Puskesmas",
path: "/admin/kesehatan/puskesmas"
},
{
id: "Kesehatan_4",
name: "Program Kesehatan",
path: "/admin/kesehatan/program-kesehatan"
},
{
id: "Kesehatan_5",
name: "Penanganan Darurat",
path: "/admin/kesehatan/penanganan-darurat"
},
{
id: "Kesehatan_6",
name: "Kontak Darurat",
path: "/admin/kesehatan/kontak-darurat"
},
{
id: "Kesehatan_7",
name: "Info Wabah/Penyakit",
path: "/admin/kesehatan/info-wabah-penyakit"
}
]
}
]
export const role3 = [
{
id: "Pendidikan",
name: "Pendidikan",
path: "",

View File

@@ -0,0 +1,9 @@
// src/app/(admin)/_com/navigationByRole.ts
import { navBar, role1, role2, role3 } from './list_PageAdmin';
export const navigationByRole = {
0: navBar, // SUPERADMIN
1: role1, // ADMIN DESA
2: role2, // ADMIN KESEHATAN
3: role3, // ADMIN PENDIDIKAN
} as const;

View File

@@ -1,6 +1,267 @@
// /* eslint-disable react-hooks/exhaustive-deps */
// 'use client'
// import colors from "@/con/colors";
// import {
// ActionIcon,
// AppShell,
// AppShellHeader,
// AppShellMain,
// AppShellNavbar,
// Burger,
// Flex,
// Group,
// Image,
// NavLink,
// ScrollArea,
// Text,
// Tooltip,
// rem
// } from "@mantine/core";
// import { useDisclosure } from "@mantine/hooks";
// import {
// IconChevronLeft,
// IconChevronRight,
// IconLogout2
// } from "@tabler/icons-react";
// import _ from "lodash";
// import Link from "next/link";
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
// import { navBar } from "./_com/list_PageAdmin";
// export default function Layout({ children }: { children: React.ReactNode }) {
// const [opened, { toggle }] = useDisclosure();
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
// const router = useRouter();
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
// return (
// <AppShell
// suppressHydrationWarning
// header={{ height: 64 }}
// navbar={{
// width: { base: 260, sm: 280, lg: 300 },
// breakpoint: 'sm',
// collapsed: {
// mobile: !opened,
// desktop: !desktopOpened,
// },
// }}
// padding="md"
// >
// <AppShellHeader
// style={{
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
// borderBottom: `1px solid ${colors["blue-button"]}20`,
// padding: '0 16px',
// }}
// px={{ base: 'sm', sm: 'md' }}
// py={{ base: 'xs', sm: 'sm' }}
// >
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
// <Flex align="center" gap="sm">
// <Image
// src="/assets/images/darmasaba-icon.png"
// alt="Logo Darmasaba"
// w={{ base: 32, sm: 40 }}
// h={{ base: 32, sm: 40 }}
// radius="md"
// loading="lazy"
// style={{
// minWidth: '32px',
// height: 'auto',
// }}
// />
// <Text
// fw={700}
// c={colors["blue-button"]}
// fz={{ base: 'md', sm: 'xl' }}
// >
// Admin Darmasaba
// </Text>
// </Flex>
// <Group gap="xs">
// {!desktopOpened && (
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
// <ActionIcon
// variant="light"
// radius="xl"
// size="lg"
// onClick={toggleDesktop}
// color={colors["blue-button"]}
// >
// <IconChevronRight />
// </ActionIcon>
// </Tooltip>
// )}
// <Burger
// opened={opened}
// onClick={toggle}
// hiddenFrom="sm"
// size="md"
// color={colors["blue-button"]}
// mr="xs"
// />
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
// <ActionIcon
// onClick={() => {
// router.push("/darmasaba");
// }}
// color={colors["blue-button"]}
// radius="xl"
// size="lg"
// variant="gradient"
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
// >
// <Image
// src="/assets/images/darmasaba-icon.png"
// alt="Logo Darmasaba"
// w={20}
// h={20}
// radius="md"
// loading="lazy"
// style={{
// minWidth: '20px',
// height: 'auto',
// }}
// />
// </ActionIcon>
// </Tooltip>
// <Tooltip label="Keluar" position="bottom" withArrow>
// <ActionIcon
// onClick={() => {
// router.push("/darmasaba");
// }}
// color={colors["blue-button"]}
// radius="xl"
// size="lg"
// variant="gradient"
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
// >
// <IconLogout2 size={22} />
// </ActionIcon>
// </Tooltip>
// </Group>
// </Group>
// </AppShellHeader>
// <AppShellNavbar
// component={ScrollArea}
// style={{
// background: "#ffffff",
// borderRight: `1px solid ${colors["blue-button"]}20`,
// }}
// p={{ base: 'xs', sm: 'sm' }}
// >
// <AppShell.Section p="sm">
// {navBar.map((v, k) => {
// const isParentActive = segments.includes(_.lowerCase(v.name));
// return (
// <NavLink
// key={k}
// defaultOpened={isParentActive}
// c={isParentActive ? colors["blue-button"] : "gray"}
// label={
// <Text fw={isParentActive ? 600 : 400} fz="sm">
// {v.name}
// </Text>
// }
// style={{
// borderRadius: rem(10),
// marginBottom: rem(4),
// transition: "background 150ms ease",
// }}
// styles={{
// root: {
// '&:hover': {
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
// },
// },
// }}
// variant="light"
// active={isParentActive}
// >
// {v.children.map((child, key) => {
// const isChildActive = segments.includes(
// _.lowerCase(child.name)
// );
// return (
// <NavLink
// key={key}
// href={child.path}
// c={isChildActive ? colors["blue-button"] : "gray"}
// label={
// <Text fw={isChildActive ? 600 : 400} fz="sm">
// {child.name}
// </Text>
// }
// styles={{
// root: {
// borderRadius: rem(8),
// marginBottom: rem(2),
// transition: 'background 150ms ease',
// padding: '6px 12px',
// '&:hover': {
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
// },
// ...(isChildActive && {
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
// }),
// },
// }}
// active={isChildActive}
// component={Link}
// />
// );
// })}
// </NavLink>
// );
// })}
// </AppShell.Section>
// <AppShell.Section py="md">
// <Group justify="end" pr="sm">
// <Tooltip
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
// position="top"
// withArrow
// >
// <ActionIcon
// variant="light"
// radius="xl"
// size="lg"
// onClick={toggleDesktop}
// color={colors["blue-button"]}
// >
// <IconChevronLeft />
// </ActionIcon>
// </Tooltip>
// </Group>
// </AppShell.Section>
// </AppShellNavbar>
// <AppShellMain
// style={{
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
// minHeight: "100vh",
// }}
// >
// {children}
// </AppShellMain>
// </AppShell>
// );
// }
'use client'
import colors from "@/con/colors";
import { authStore } from "@/store/authStore";
import {
ActionIcon,
AppShell,
@@ -8,9 +269,11 @@ import {
AppShellMain,
AppShellNavbar,
Burger,
Center,
Flex,
Group,
Image,
Loader,
NavLink,
ScrollArea,
Text,
@@ -26,14 +289,165 @@ import {
import _ from "lodash";
import Link from "next/link";
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
import { navBar } from "./_com/list_PageAdmin";
import { useEffect, useState } from "react";
import { navigationByRole } from "./_com/navigationByRole";
import { useSnapshot } from "valtio";
import { toast } from "react-toastify";
export default function Layout({ children }: { children: React.ReactNode }) {
const [opened, { toggle }] = useDisclosure();
const [loading, setLoading] = useState(true);
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const router = useRouter();
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
const { user } = useSnapshot(authStore);
console.log("Current user in store:", user);
// ✅ Fetch user dari backend jika belum ada di store
useEffect(() => {
if (user) {
setLoading(false);
return;
} // Sudah ada → jangan fetch
const fetchUser = async () => {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
if (data.user) {
authStore.setUser({
id: data.user.id,
name: data.user.name,
roleId: Number(data.user.roleId),
});
} else {
authStore.setUser(null);
router.replace('/login');
}
} catch (error) {
console.error('Gagal memuat data pengguna:', error);
authStore.setUser(null);
router.replace('/login');
} finally {
setLoading(false);
}
};
fetchUser();
}, [user, router]); // ✅ Sekarang 'user' terdefinisi
// ✅ Polling untuk cek perubahan role setiap 30 detik
// Di layout.tsx - useEffect polling
useEffect(() => {
if (!user?.id) return;
const checkRoleUpdate = async () => {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
// ✅ Session tidak valid (sudah dihapus karena role berubah)
if (!data.success || !data.user) {
console.log('⚠️ Session tidak valid, logout...');
authStore.setUser(null);
// Clear cookie manual (backup)
document.cookie = `${process.env.NEXT_PUBLIC_SESSION_KEY}=; Max-Age=0; path=/;`;
toast.info('Role Anda telah diubah. Silakan login kembali.', {
autoClose: 5000,
});
router.push('/login');
return;
}
// Cek perubahan roleId (seharusnya tidak sampai sini jika session dihapus)
const currentRoleId = Number(data.user.roleId);
if (currentRoleId !== user.roleId) {
console.log('🔄 Role berubah! Dari', user.roleId, 'ke', currentRoleId);
// Update store
authStore.setUser({
id: data.user.id,
name: data.user.name || data.user.username,
roleId: currentRoleId,
});
// Redirect ke halaman default role baru
let redirectPath = '/admin';
switch (currentRoleId) {
case 0:
case 1:
redirectPath = '/admin/landing-page/profil/program-inovasi';
break;
case 2:
redirectPath = '/admin/kesehatan/posyandu';
break;
case 3:
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
break;
}
toast.info('Role Anda telah diubah. Mengalihkan halaman...', {
autoClose: 5000,
});
router.push(redirectPath);
// Reload untuk clear semua state
setTimeout(() => {
window.location.reload();
}, 500);
}
} catch (error) {
console.error('❌ Error checking role update:', error);
}
};
// ✅ Polling setiap 10 detik (lebih responsif dari 30 detik)
const interval = setInterval(checkRoleUpdate, 10000);
// Juga cek saat window focus (user kembali ke tab)
const handleFocus = () => {
checkRoleUpdate();
};
window.addEventListener('focus', handleFocus);
return () => {
clearInterval(interval);
window.removeEventListener('focus', handleFocus);
};
}, [user, router]);
if (loading) {
return (
<AppShell>
<AppShellMain>
<Center h="100vh">
<Loader />
</Center>
</AppShellMain>
</AppShell>
);
}
// ✅ Ambil menu berdasarkan roleId
const currentNav = user?.roleId !== undefined
? (navigationByRole[user.roleId as keyof typeof navigationByRole] || [])
: [];
const handleLogout = () => {
authStore.setUser(null);
document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`;
router.push('/login');
};
return (
<AppShell
suppressHydrationWarning
@@ -115,13 +529,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
variant="gradient"
gradient={{ from: colors["blue-button"], to: "#228be6" }}
>
<Image
src="/assets/images/darmasaba-icon.png"
alt="Logo Darmasaba"
w={20}
h={20}
radius="md"
loading="lazy"
<Image
src="/assets/images/darmasaba-icon.png"
alt="Logo Darmasaba"
w={20}
h={20}
radius="md"
loading="lazy"
style={{
minWidth: '20px',
height: 'auto',
@@ -131,9 +545,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</Tooltip>
<Tooltip label="Keluar" position="bottom" withArrow>
<ActionIcon
onClick={() => {
router.push("/darmasaba");
}}
onClick={handleLogout}
color={colors["blue-button"]}
radius="xl"
size="lg"
@@ -156,7 +568,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
p={{ base: 'xs', sm: 'sm' }}
>
<AppShell.Section p="sm">
{navBar.map((v, k) => {
{currentNav.map((v, k) => {
const isParentActive = segments.includes(_.lowerCase(v.name));
return (
@@ -255,3 +667,4 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</AppShell>
);
}

View File

@@ -0,0 +1,38 @@
// /api/user/delete.ts
import prisma from '@/lib/prisma';
import { Context } from 'elysia';
export default async function userDelete(context: Context) {
const { id } = context.params as { id: string };
try {
// Cek user dulu
const existingUser = await prisma.user.findUnique({
where: { id },
});
if (!existingUser) {
return {
success: false,
message: 'User tidak ditemukan',
};
}
// Hard delete (hapus permanen)
const deletedUser = await prisma.user.delete({
where: { id },
});
return {
success: true,
message: 'User berhasil dihapus permanen',
data: deletedUser,
};
} catch (error) {
console.error('Error delete user:', error);
return {
success: false,
message: 'Terjadi kesalahan saat menghapus user',
};
}
}

View File

@@ -14,6 +14,21 @@ const User = new Elysia({ prefix: "/api/user" })
id: t.String(),
}),
}) // pakai PUT untuk soft delete
.put("/updt", userUpdate);
.put(
"/updt",
userUpdate,
{
body: t.Object({
id: t.String(),
isActive: t.Optional(t.Boolean()),
roleId: t.Optional(t.String()),
})
}
)
.put("/delUser/:id", userDelete, {
params: t.Object({
id: t.String(),
}),
});
export default User;

View File

@@ -3,7 +3,6 @@ import { Context } from "elysia";
type FormCreate = {
name: string;
permissions: string[];
}
export default async function roleCreate(context: Context) {
@@ -13,7 +12,6 @@ export default async function roleCreate(context: Context) {
const result = await prisma.role.create({
data: {
name: body.name,
permissions: body.permissions,
},
});
return {

View File

@@ -13,7 +13,6 @@ const Role = new Elysia({
.post("/create", roleCreate, {
body: t.Object({
name: t.String(),
permissions: t.Array(t.String()),
}),
})
@@ -27,7 +26,6 @@ const Role = new Elysia({
.put("/:id", roleUpdate, {
body: t.Object({
name: t.String(),
permissions: t.Array(t.String()),
}),
})
.delete("/del/:id", roleDelete);

View File

@@ -3,7 +3,6 @@ import { Context } from "elysia";
type FormUpdate = {
name: string;
permissions: string[];
}
export default async function roleUpdate(context: Context) {
@@ -15,7 +14,6 @@ export default async function roleUpdate(context: Context) {
where: { id },
data: {
name: body.name,
permissions: body.permissions,
},
});
return {

View File

@@ -4,7 +4,11 @@ import { Context } from "elysia";
export default async function userUpdate(context: Context) {
try {
const { id, isActive } = await context.body as { id: string, isActive: boolean };
const { id, isActive, roleId } = await context.body as {
id: string,
isActive?: boolean,
roleId?: string
};
if (!id) {
return {
@@ -13,28 +17,100 @@ export default async function userUpdate(context: Context) {
};
}
// Optional: cek apakah roleId valid
if (roleId) {
const cekRole = await prisma.role.findUnique({
where: { id: roleId }
});
if (!cekRole) {
return {
success: false,
message: "Role tidak ditemukan",
};
}
}
// ✅ CEK: Apakah roleId berubah?
let isRoleChanged = false;
let oldRoleId: string | null = null;
if (roleId) {
const currentUser = await prisma.user.findUnique({
where: { id },
select: {
roleId: true,
username: true,
}
});
if (currentUser && currentUser.roleId !== roleId) {
isRoleChanged = true;
oldRoleId = currentUser.roleId;
console.log(`🔄 Role berubah untuk ${currentUser.username}: ${oldRoleId}${roleId}`);
}
}
// Update user
const updatedUser = await prisma.user.update({
where: { id },
data: { isActive },
data: {
...(isActive !== undefined && { isActive }),
...(roleId && { roleId }),
},
select: {
id: true,
username: true,
nomor: true,
isActive: true,
roleId: true,
updatedAt: true,
role: {
select: {
id: true,
name: true,
}
}
}
});
// ✅ FORCE LOGOUT: Hapus UserSession jika role berubah
if (isRoleChanged) {
try {
const deletedSessions = await prisma.userSession.deleteMany({
where: { userId: id }
});
console.log(`🔒 Force logout user ${updatedUser.username} (${id})`);
console.log(` Deleted ${deletedSessions.count} session(s)`);
console.log(` Role: ${oldRoleId}${roleId}`);
} catch (sessionError: any) {
// Jika UserSession tidak ditemukan (user belum pernah login), skip error
if (sessionError.code !== 'P2025') {
console.error("⚠️ Error menghapus session:", sessionError);
} else {
console.log(` User ${updatedUser.username} belum pernah login`);
}
}
}
// ✅ Response dengan info tambahan
return {
success: true,
message: `User berhasil ${isActive ? "diaktifkan" : "dinonaktifkan"}`,
message: isRoleChanged
? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.`
: "User berhasil diupdate",
data: updatedUser,
roleChanged: isRoleChanged, // Info untuk frontend
oldRoleId: oldRoleId,
newRoleId: roleId,
};
} catch (e: any) {
console.error("Error update user:", e);
console.error("Error update user:", e);
return {
success: false,
message: "Gagal mengupdate status user",
message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
};
}
}
}

View File

@@ -54,7 +54,7 @@ export const apiFetchRegister = async ({
const cleanPhone = nomor.replace(/\D/g, '');
if (cleanPhone.length < 10) throw new Error('Nomor tidak valid');
const response = await fetch("/api/auth/send-otp-register", {
const response = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }),
@@ -86,38 +86,6 @@ export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => {
return data;
};
// export const apiFetchVerifyOtp = async ({
// nomor,
// otp,
// kodeId
// }: {
// nomor: string;
// otp: string;
// kodeId: string;
// }) => {
// if (!nomor || !otp || !kodeId) {
// throw new Error('Data verifikasi tidak lengkap');
// }
// if (!/^\d{4,6}$/.test(otp)) {
// throw new Error('Kode OTP harus 4-6 digit angka');
// }
// const response = await fetch('/api/auth/verify-otp', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ nomor, otp, kodeId }),
// });
// const data = await response.json();
// if (!response.ok) {
// throw new Error(data.message || 'Verifikasi OTP gagal');
// }
// return data;
// };
export const apiFetchVerifyOtp = async ({
nomor,
otp,

View File

@@ -1,9 +1,11 @@
// app/api/auth/_lib/session_create.ts
import { cookies } from "next/headers";
import { encrypt } from "./encrypt";
import prisma from "@/lib/prisma";
export async function sessionCreate({
sessionKey,
exp = "7 year",
exp = "30 day",
jwtSecret,
user,
}: {
@@ -30,12 +32,59 @@ export async function sessionCreate({
throw new Error("Token generation failed");
}
// ✅ HYBRID: Simpan token ke database UserSession
const userId = user.id as string;
if (userId) {
try {
// Hapus session lama user ini (logout device lain)
await prisma.userSession.deleteMany({
where: { userId },
});
// Parse expiration
const expiresDate = new Date();
const expMatch = exp.match(/(\d+)\s*(day|year)/);
if (expMatch) {
const [, num, unit] = expMatch;
const amount = parseInt(num);
if (unit === 'year') {
expiresDate.setFullYear(expiresDate.getFullYear() + amount);
} else if (unit === 'day') {
expiresDate.setDate(expiresDate.getDate() + amount);
}
} else {
// Default 30 hari
expiresDate.setDate(expiresDate.getDate() + 30);
}
// Buat session baru di database
await prisma.userSession.create({
data: {
userId,
token, // JWT token disimpan
expires: expiresDate,
active: true,
},
});
console.log(`✅ Session created for user ${userId}`);
} catch (dbError) {
console.error("⚠️ Error menyimpan session ke database:", dbError);
// Tetap lanjut meski gagal simpan ke DB (fallback ke JWT only)
}
}
// Set cookie
const cookieStore = await cookies();
cookieStore.set(sessionKey, token, {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: process.env.NODE_ENV === "production",
maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik
});
return token;

View File

@@ -0,0 +1,42 @@
// app/api/auth/_lib/session_delete.ts
import { cookies } from "next/headers";
import prisma from "@/lib/prisma";
/**
* Hapus session dari database dan cookie
*/
export async function sessionDelete({
sessionKey,
userId,
}: {
sessionKey: string;
userId?: string;
}): Promise<boolean> {
try {
const cookieStore = await cookies();
const token = cookieStore.get(sessionKey)?.value;
// Hapus dari database
if (token) {
const deleted = await prisma.userSession.deleteMany({
where: { token },
});
console.log(`🗑️ Deleted ${deleted.count} session(s) by token`);
} else if (userId) {
// Fallback: hapus berdasarkan userId
const deleted = await prisma.userSession.deleteMany({
where: { userId },
});
console.log(`🗑️ Deleted ${deleted.count} session(s) for user ${userId}`);
}
// Hapus cookie
cookieStore.delete(sessionKey);
console.log('✅ Session deleted successfully');
return true;
} catch (error) {
console.error("❌ Error deleting session:", error);
return false;
}
}

View File

@@ -0,0 +1,90 @@
// app/api/auth/_lib/session_verify.ts
import { cookies } from 'next/headers';
import { decrypt } from './decrypt';
import prisma from '@/lib/prisma';
/**
* Verifikasi session hybrid:
* 1. Decrypt JWT token
* 2. Cek apakah token masih ada di database (untuk force logout)
* 3. Return data user terbaru dari database
*/
export async function verifySession(): Promise<Record<string, unknown> | null> {
try {
const sessionKey = process.env.BASE_SESSION_KEY;
if (!sessionKey) {
throw new Error('BASE_SESSION_KEY tidak ditemukan di environment');
}
const jwtSecret = process.env.BASE_TOKEN_KEY;
if (!jwtSecret) {
throw new Error('BASE_TOKEN_KEY tidak ditemukan di environment');
}
const cookieStore = await cookies();
const token = cookieStore.get(sessionKey)?.value;
if (!token) {
return null;
}
// Step 1: Decrypt JWT
const jwtUser = await decrypt({ token, jwtSecret });
if (!jwtUser || !jwtUser.id) {
console.log('⚠️ JWT decrypt failed atau tidak ada user ID');
return null;
}
// Step 2: Cek database UserSession (untuk force logout)
try {
const dbSession = await prisma.userSession.findFirst({
where: {
userId: jwtUser.id as string,
token: token,
active: true,
OR: [
{ expires: null },
{ expires: { gte: new Date() } },
],
},
include: {
User: {
select: {
id: true,
username: true,
nomor: true,
roleId: true,
isActive: true,
},
},
},
});
// Token tidak ditemukan di database = sudah dihapus (force logout)
if (!dbSession) {
console.log('⚠️ Token valid tapi sudah dihapus dari database (force logout)');
return null;
}
// Step 3: Return data user terbaru dari database
// Ini penting agar roleId selalu update
return {
id: dbSession.User.id,
username: dbSession.User.username,
nomor: dbSession.User.nomor,
roleId: dbSession.User.roleId,
isActive: dbSession.User.isActive,
};
} catch (dbError) {
console.error("⚠️ Error cek database session:", dbError);
// Fallback: jika database error, tetap pakai JWT
return jwtUser;
}
} catch (error) {
console.warn('❌ Session verification failed:', error);
return null;
}
}

View File

@@ -5,35 +5,95 @@ import { sessionCreate } from "../_lib/session_create";
export async function POST(req: Request) {
try {
const { nomor, username, kodeId } = await req.json();
const { nomor, username, kodeId, roleId } = await req.json();
// Verifikasi OTP (sama seperti verify-otp)
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
if (!otpRecord?.isActive || otpRecord.nomor !== nomor) {
return NextResponse.json({ success: false, message: 'OTP tidak valid' }, { status: 400 });
// Validasi input
if (!nomor || !username || !kodeId) {
return NextResponse.json(
{ success: false, message: "Data tidak lengkap" },
{ status: 400 }
);
}
// Buat user
const user = await prisma.user.create({
data: { username, nomor, isActive: true }
// Verifikasi OTP
const otpRecord = await prisma.kodeOtp.findUnique({
where: { id: kodeId },
});
if (!otpRecord?.isActive || otpRecord.nomor !== nomor) {
return NextResponse.json(
{ success: false, message: "OTP tidak valid" },
{ status: 400 }
);
}
// Cek apakah username sudah dipakai
const existingUser = await prisma.user.findUnique({
where: { username },
});
if (existingUser) {
return NextResponse.json(
{ success: false, message: "Username sudah digunakan" },
{ status: 400 }
);
}
// Buat user baru
const newUser = await prisma.user.create({
data: {
username,
nomor,
roleId: roleId || "1", // Default role
isActive: false, // Menunggu approval
},
});
// Nonaktifkan OTP
await prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } });
// Buat session
const token = await sessionCreate({
sessionKey: process.env.BASE_SESSION_KEY!,
jwtSecret: process.env.BASE_TOKEN_KEY!,
user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true },
await prisma.kodeOtp.update({
where: { id: kodeId },
data: { isActive: false },
});
// ✅ CREATE SESSION (JWT + Database)
try {
await sessionCreate({
sessionKey: process.env.BASE_SESSION_KEY!,
jwtSecret: process.env.BASE_TOKEN_KEY!,
exp: "30 day",
user: {
id: newUser.id,
nomor: newUser.nomor,
username: newUser.username,
roleId: newUser.roleId,
isActive: false, // User baru belum aktif
},
});
} catch (sessionError) {
console.error("❌ Error creating session:", sessionError);
return NextResponse.json(
{ success: false, message: "Gagal membuat session" },
{ status: 500 }
);
}
return NextResponse.json({
success: true,
message: "Registrasi berhasil. Menunggu persetujuan admin.",
user: {
id: newUser.id,
name: newUser.username,
roleId: newUser.roleId,
isActive: false,
},
});
const response = NextResponse.json({ success: true, roleId: user.roleId });
response.cookies.set(process.env.BASE_SESSION_KEY!, token, { /* options */ });
return response;
} catch (error) {
console.error('Finalize Registration Error:', error);
return NextResponse.json({ success: false, message: 'Registrasi gagal' }, { status: 500 });
console.error("❌ Finalize Registration Error:", error);
return NextResponse.json(
{ success: false, message: "Registrasi gagal" },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}

View File

@@ -0,0 +1,35 @@
// app/api/auth/logout/route.ts
import { NextResponse } from "next/server";
import { sessionDelete } from "../_lib/session_delete";
export async function POST() {
try {
const deleted = await sessionDelete({
sessionKey: process.env.BASE_SESSION_KEY!,
});
if (deleted) {
return NextResponse.json({
success: true,
message: "Logout berhasil",
});
} else {
return NextResponse.json(
{
success: false,
message: "Gagal logout",
},
{ status: 500 }
);
}
} catch (error) {
console.error("❌ Logout Error:", error);
return NextResponse.json(
{
success: false,
message: "Terjadi kesalahan saat logout",
},
{ status: 500 }
);
}
}

View File

@@ -1,30 +1,45 @@
import prisma from "@/lib/prisma";
import { NextRequest } from "next/server";
// Jika pakai custom session (bukan next-auth), ganti dengan logic session-mu
// app/api/auth/me/route.ts
import { NextResponse } from 'next/server';
import { verifySession } from '../_lib/session_verify';
export async function GET(req: NextRequest) {
// 🔸 GANTI DENGAN LOGIC SESSION-MU
// Contoh: jika kamu simpan user.id di cookie atau JWT
const userId = req.cookies.get("desadarmasaba_user_id")?.value; // sesuaikan
export async function GET() {
try {
// ✅ Verify session (hybrid: JWT + Database)
const user = await verifySession();
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
if (!user) {
return NextResponse.json(
{
success: false,
message: "Session tidak valid",
user: null
},
{ status: 401 }
);
}
// Data user sudah fresh dari database (via verifySession)
return NextResponse.json({
success: true,
user: {
id: user.id,
name: user.username,
username: user.username,
nomor: user.nomor,
roleId: user.roleId,
isActive: user.isActive,
},
});
} catch (error) {
console.error("❌ Error in /api/auth/me:", error);
return NextResponse.json(
{
success: false,
message: "Terjadi kesalahan",
user: null
},
{ status: 500 }
);
}
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
nomor: true,
isActive: true,
role: { select: { name: true } },
},
});
if (!user) {
return Response.json({ error: "User not found" }, { status: 404 });
}
return Response.json({ user });
}

View File

@@ -1,121 +1,50 @@
// app/api/auth/register/route.ts
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import { randomOTP } from '../_lib/randomOTP'; // pastikan ada
export async function POST(req: Request) {
try {
// Terima langsung properti, bukan { data: { ... } }
const { username, nomor } = await req.json();
// Validasi input
if (!username || !nomor) {
return NextResponse.json(
{ success: false, message: 'Data tidak lengkap' },
{ status: 400 }
);
return NextResponse.json({ success: false, message: 'Data tidak lengkap' }, { status: 400 });
}
// // Validasi OTP: pastikan berisi digit saja
// const cleanOtp = otp.toString().trim();
// if (!/^\d{4,6}$/.test(cleanOtp)) {
// return NextResponse.json(
// { success: false, message: 'Kode OTP tidak valid' },
// { status: 400 }
// );
// }
// const receivedOtp = parseInt(cleanOtp, 10);
// if (isNaN(receivedOtp)) {
// return NextResponse.json(
// { success: false, message: 'Kode OTP tidak valid' },
// { status: 400 }
// );
// }
// // Cari OTP record
// const otpRecord = await prisma.kodeOtp.findUnique({
// where: { id: kodeId },
// });
// if (!otpRecord) {
// return NextResponse.json(
// { success: false, message: 'Kode verifikasi tidak valid' },
// { status: 400 }
// );
// }
// if (!otpRecord.isActive) {
// return NextResponse.json(
// { success: false, message: 'Kode verifikasi sudah kadaluarsa' },
// { status: 400 }
// );
// }
// if (otpRecord.otp !== receivedOtp) {
// return NextResponse.json(
// { success: false, message: 'Kode OTP salah' },
// { status: 400 }
// );
// }
// if (otpRecord.nomor !== nomor) {
// return NextResponse.json(
// { success: false, message: 'Nomor tidak sesuai' },
// { status: 400 }
// );
// }
// Cek duplikat nomor
const existingUser = await prisma.user.findUnique({
where: { nomor },
});
if (existingUser) {
return NextResponse.json(
{ success: false, message: 'Nomor sudah terdaftar' },
{ status: 409 }
);
// Cek duplikat
if (await prisma.user.findUnique({ where: { nomor } })) {
return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 });
}
if (await prisma.user.findUnique({ where: { username } })) {
return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
}
// Cek username unik (pastikan ada @unique di schema!)
const existingByUsername = await prisma.user.findUnique({
where: { username },
});
// ✅ Generate dan kirim OTP
const codeOtp = randomOTP();
const otpNumber = Number(codeOtp);
if (existingByUsername) {
return NextResponse.json(
{ success: false, message: 'Username sudah digunakan' },
{ status: 409 }
);
const waMessage = `Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
const waRes = await fetch(waUrl);
const waData = await waRes.json();
if (waData.status !== "success") {
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP via WhatsApp' }, { status: 400 });
}
// Buat user
const newUser = await prisma.user.create({
data: {
username: username.trim(),
nomor,
isActive: false,
// roleId default "1"
},
// ✅ Simpan OTP ke database
const otpRecord = await prisma.kodeOtp.create({
data: { nomor, otp: otpNumber, isActive: true }
});
// // Nonaktifkan OTP
// await prisma.kodeOtp.update({
// where: { id: kodeId },
// data: { isActive: false },
// });
// ✅ Kembalikan kodeId (jangan buat user di sini!)
return NextResponse.json({
success: true,
message: 'Pendaftaran berhasil. Menunggu persetujuan admin.',
userId: newUser.id,
message: 'Kode verifikasi dikirim',
kodeId: otpRecord.id,
});
} catch (error) {
console.error('Registration error:', error);
return NextResponse.json(
{ success: false, message: 'Terjadi kesalahan saat pendaftaran' },
{ status: 500 }
);
console.error('Register OTP Error:', error);
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 500 });
} finally {
await prisma.$disconnect();
}

View File

@@ -4,13 +4,6 @@ import { NextResponse } from "next/server";
import { sessionCreate } from "../_lib/session_create";
export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
{ success: false, message: "Method Not Allowed" },
{ status: 405 }
);
}
try {
const { nomor, otp, kodeId } = await req.json();
@@ -41,7 +34,7 @@ export async function POST(req: Request) {
);
}
// Pastikan tipe data cocok (OTP di DB = number)
// Validasi OTP
const receivedOtp = Number(otp);
if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
return NextResponse.json(
@@ -76,26 +69,22 @@ export async function POST(req: Request) {
);
}
if (!user.isActive) {
return NextResponse.json(
{ success: false, message: "Akun belum disetujui oleh admin" },
{ status: 403 }
);
}
// Buat session
const token = await sessionCreate({
sessionKey: process.env.BASE_SESSION_KEY!,
jwtSecret: process.env.BASE_TOKEN_KEY!, // ✅
user: {
id: user.id,
nomor: user.nomor,
username: user.username,
roleId: user.roleId,
isActive: user.isActive,
},
});
if (!token) {
// ✅ CREATE SESSION (JWT + Database)
try {
await sessionCreate({
sessionKey: process.env.BASE_SESSION_KEY!,
jwtSecret: process.env.BASE_TOKEN_KEY!,
exp: "30 day",
user: {
id: user.id,
nomor: user.nomor,
username: user.username,
roleId: user.roleId,
isActive: user.isActive,
},
});
} catch (sessionError) {
console.error("❌ Error creating session:", sessionError);
return NextResponse.json(
{ success: false, message: "Gagal membuat session" },
{ status: 500 }
@@ -108,27 +97,25 @@ export async function POST(req: Request) {
data: { isActive: false },
});
// Set cookie & respons
const response = NextResponse.json(
{
success: true,
message: "Berhasil login",
roleId: user.roleId,
},
{ status: 200 }
);
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
path: "/",
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
httpOnly: true, // 🔒 lebih aman
maxAge: 30 * 24 * 60 * 60,
// Update lastLogin
await prisma.user.update({
where: { id: user.id },
data: { lastLogin: new Date() },
});
return NextResponse.json({
success: true,
message: user.isActive ? "Berhasil login" : "Menunggu persetujuan",
user: {
id: user.id,
name: user.username,
roleId: user.roleId,
isActive: user.isActive,
},
});
return response;
} catch (error) {
console.error("Verify OTP Error:", error);
console.error("Verify OTP Error:", error);
return NextResponse.json(
{ success: false, message: "Terjadi kesalahan saat verifikasi" },
{ status: 500 }

View File

@@ -19,37 +19,42 @@ export default function WaitingRoom() {
// const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
const interval = setInterval(async () => {
try {
const data = await fetchUser();
if (!isMounted) return;
setUser(data.user);
useEffect(() => {
let isMounted = true;
const interval = setInterval(async () => {
try {
const data = await fetchUser();
if (!isMounted) return;
// Jika sudah aktif, redirect ke dashboard admin
if (data.user.isActive) {
clearInterval(interval);
router.push('/admin'); // atau /dashboard
}
} catch (err: any) {
if (!isMounted) return;
setError(err.message || 'Gagal memuat status');
const currentUser = data.user;
setUser(currentUser);
// ✅ Sekarang isActive tersedia!
if (currentUser?.isActive) {
clearInterval(interval);
// Redirect ke login jika unauthorized
if (err.message === 'Unauthorized') {
router.push('/login');
// Redirect ke halaman admin sesuai role
if (currentUser.roleId === 0) {
router.push('/admin/landing-page/profil/program-inovasi');
} else {
router.push('/admin'); // atau halaman default role
}
}
}, 2000); // Cek setiap 2 detik
// Cleanup
return () => {
isMounted = false;
} catch (err: any) {
if (!isMounted) return;
setError(err.message || 'Gagal memuat status');
clearInterval(interval);
};
}, [router]);
if (err.message === 'Unauthorized') {
router.push('/login');
}
}
}, 2000);
return () => {
isMounted = false;
clearInterval(interval);
};
}, [router]);
if (error) {
return (

17
src/store/authStore.ts Normal file
View File

@@ -0,0 +1,17 @@
import { proxy } from 'valtio';
export type User = {
id: string;
name: string;
roleId?: number; // 0, 1, 2, 3
};
export const authStore = proxy<{
user: User | null;
setUser: (user: User | null) => void;
}>({
user: null,
setUser(user) {
authStore.user = user;
},
});