diff --git a/prisma/data/user/roles.json b/prisma/data/user/roles.json index 423965fc..4d2a1046 100644 --- a/prisma/data/user/roles.json +++ b/prisma/data/user/roles.json @@ -1,24 +1,30 @@ [ { "id": "0", + "name": "DEVELOPER", + "description": "Developer", + "isActive": true + }, + { + "id": "1", "name": "SUPER ADMIN", "description": "Administrator", "isActive": true }, { - "id": "1", + "id": "2", "name": "ADMIN DESA", "description": "Administrator Desa", "isActive": true }, { - "id": "2", + "id": "3", "name": "ADMIN KESEHATAN", "description": "Administrator Bidang Kesehatan", "isActive": true }, { - "id": "3", + "id": "4", "name": "ADMIN PENDIDIKAN", "description": "Administrator Bidang Pendidikan", "isActive": true diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 91d234bd..954a144b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2163,20 +2163,21 @@ enum StatusPeminjaman { // ========================================= USER ========================================= // model User { - id String @id @default(cuid()) - username String @unique - nomor String @unique - role Role @relation(fields: [roleId], references: [id]) - roleId String @default("1") - instansi String? - UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll) - isActive Boolean @default(true) + id String @id @default(cuid()) + username String + nomor String @unique + roleId String @default("2") + isActive Boolean @default(false) + sessionInvalid Boolean @default(false) lastLogin DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - sessionInvalid Boolean @default(false) - UserMenuAccess UserMenuAccess[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + sessions UserSession[] // ✅ Relasi one-to-many + role Role @relation(fields: [roleId], references: [id]) + menuAccesses UserMenuAccess[] + + @@map("users") } model Role { @@ -2203,13 +2204,18 @@ model KodeOtp { model UserSession { id String @id @default(cuid()) - token String - expires DateTime? + token String @db.Text // ✅ JWT bisa panjang + expiresAt DateTime // ✅ Ubah jadi expiresAt (konsisten) active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - User User @relation(fields: [userId], references: [id]) - userId String @unique + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String // ✅ HAPUS @unique - user bisa punya multiple sessions + + @@index([userId]) // ✅ Index untuk query cepat + @@index([token]) // ✅ Index untuk verify cepat + @@map("user_sessions") } model UserMenuAccess { diff --git a/src/app/admin/(dashboard)/_state/user/user-state.ts b/src/app/admin/(dashboard)/_state/user/user-state.ts index 2bc42f7e..e3864904 100644 --- a/src/app/admin/(dashboard)/_state/user/user-state.ts +++ b/src/app/admin/(dashboard)/_state/user/user-state.ts @@ -118,7 +118,7 @@ const userState = proxy({ console.error("Gagal delete user:", error); toast.error("Terjadi kesalahan saat menghapus user"); } finally { - userState.delete.loading = false; + userState.deleteUser.loading = false; } }, }, diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx index a8c21414..192eba0a 100644 --- a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx +++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx @@ -1,10 +1,17 @@ -// - 'use client'; -import { apiFetchOtpData, apiFetchVerifyOtp } from '@/app/api/auth/_lib/api_fetch_auth'; import colors from '@/con/colors'; -import { Box, Button, Center, Loader, Paper, PinInput, Stack, Text, Title } from '@mantine/core'; +import { + Box, + Button, + Center, + Loader, + Paper, + PinInput, + Stack, + Text, + Title, +} from '@mantine/core'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; @@ -17,8 +24,14 @@ export default function Validasi() { const [loading, setLoading] = useState(false); const [isLoading, setIsLoading] = useState(true); const [kodeId, setKodeId] = useState(null); + const [isRegistrationFlow, setIsRegistrationFlow] = useState(false); // Tambahkan flag + + // Cek apakah ini alur registrasi + useEffect(() => { + const storedUsername = localStorage.getItem('auth_username'); + setIsRegistrationFlow(!!storedUsername); + }, []); - // Inisialisasi data OTP useEffect(() => { const storedKodeId = localStorage.getItem('auth_kodeId'); if (!storedKodeId) { @@ -28,11 +41,12 @@ export default function Validasi() { } setKodeId(storedKodeId); - const loadOtpData = async () => { try { - const result = await apiFetchOtpData({ kodeId: storedKodeId }); - if (result.success && result.data?.nomor) { + const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`); + const result = await res.json(); + + if (res.ok && result.data?.nomor) { setNomor(result.data.nomor); } else { throw new Error('Data OTP tidak valid'); @@ -45,82 +59,21 @@ export default function Validasi() { setIsLoading(false); } }; - loadOtpData(); }, [router]); - // Verifikasi OTP const handleVerify = async () => { if (!kodeId || !nomor || otp.length < 4) return; setLoading(true); try { - const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId }); - - if (!verifyResult.success) { - // Registrasi baru? - if ( - verifyResult.status === 404 && - verifyResult.message?.includes('Akun tidak ditemukan') - ) { - await handleNewRegistration(); - return; - } - - // Error lain - toast.error(verifyResult.message || 'Verifikasi gagal'); - return; + if (isRegistrationFlow) { + // 🔑 Alur REGISTRASI + await handleRegistrationVerification(); + } else { + // 🔑 Alur LOGIN + await handleLoginVerification(); } - - // ✅ 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('Error saat verifikasi:', error); toast.error('Terjadi kesalahan sistem'); @@ -129,38 +82,111 @@ export default function Validasi() { } }; - // Registrasi baru - const handleNewRegistration = async () => { + // ✅ Verifikasi OTP untuk REGISTRASI + const handleRegistrationVerification = async () => { const username = localStorage.getItem('auth_username'); if (!username) { - toast.error('Data registrasi tidak ditemukan'); + toast.error('Data registrasi tidak ditemukan. Silakan ulangi dari awal.'); return; } - try { - const res = await fetch('/api/auth/finalize-registration', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nomor, username, otp, kodeId }), - }); + // ✅ Validasi format + const cleanNomor = nomor?.replace(/\D/g, '') ?? ''; + if (cleanNomor.length < 10) { + toast.error('Nomor tidak valid'); + return; + } - const data = await res.json(); + if (username.trim().length < 5) { + toast.error('Username minimal 5 karakter'); + return; + } - if (data.success) { - // Set user sementara (tanpa roleId, akan diisi saat approve) - authStore.setUser({ - id: 'pending', - name: username, - roleId: 1, - }); - cleanupStorage(); - router.replace('/waiting-room'); - } else { - toast.error(data.message || 'Registrasi gagal'); - } - } catch (error) { - console.error('Error registrasi:', error); - toast.error('Gagal menyelesaikan registrasi'); + // 1. Verifikasi OTP via endpoint register + const verifyRes = await fetch('/api/auth/verify-otp-register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }), + }); + + const verifyData = await verifyRes.json(); + + if (!verifyRes.ok) { + toast.error(verifyData.message || 'Verifikasi OTP gagal'); + return; + } + + // 2. Finalisasi registrasi + const finalizeRes = await fetch('/api/auth/finalize-registration', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nomor, username, kodeId }), // 🔴 Tidak perlu kirim `otp` ke sini + }); + + const finalizeData = await finalizeRes.json(); + + if (!finalizeRes.ok) { + toast.error(finalizeData.message || 'Registrasi gagal'); + return; + } + + // 3. Set user & redirect + authStore.setUser({ + id: finalizeData.user.id, + name: finalizeData.user.name, + roleId: Number(finalizeData.user.roleId), + }); + + cleanupStorage(); + router.replace('/waiting-room'); + }; + + // ✅ Verifikasi OTP untuk LOGIN + const handleLoginVerification = async () => { + const loginRes = await fetch('/api/auth/verify-otp-login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nomor, otp, kodeId }), + }); + + const loginData = await loginRes.json(); + + if (!loginRes.ok) { + toast.error(loginData.message || 'Verifikasi gagal'); + return; + } + + const { id, name, roleId, isActive } = loginData.user; + + authStore.setUser({ + id, + name: name || 'User', + roleId: Number(roleId), + }); + + cleanupStorage(); + + if (!isActive) { + router.replace('/waiting-room'); + return; + } + + const redirectPath = getRedirectPath(Number(roleId)); + router.replace(redirectPath); + }; + + const getRedirectPath = (roleId: number): string => { + switch (roleId) { + case 0: // DEVELOPER + case 1: // SUPERADMIN + case 2: // ADMIN_DESA + return '/admin/landing-page/profil/program-inovasi'; + case 3: // ADMIN_KESEHATAN + return '/admin/kesehatan/posyandu'; + case 4: // ADMIN_PENDIDIKAN + return '/admin/pendidikan/info-sekolah/jenjang-pendidikan'; + default: + return '/admin'; } }; @@ -173,7 +199,7 @@ export default function Validasi() { const handleResend = async () => { if (!nomor) return; try { - const res = await fetch('/api/auth/resend-otp', { + const res = await fetch('/api/auth/resend', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nomor }), @@ -190,7 +216,6 @@ export default function Validasi() { } }; - // Loading if (isLoading) { return ( @@ -209,7 +234,7 @@ export default function Validasi() { - Kode Verifikasi + {isRegistrationFlow ? 'Verifikasi Registrasi' : 'Verifikasi Login'} Kami telah mengirim kode ke nomor {nomor} diff --git a/src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts b/src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts index fdcab5f6..83cc2244 100644 --- a/src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts +++ b/src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts @@ -1,5 +1,5 @@ // src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts -import { navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin'; +import { devBar, navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin'; export function getNavbar({ roleId, @@ -14,9 +14,10 @@ export function getNavbar({ } // Fallback ke role-based - if (roleId === 0) return navBar; - if (roleId === 1) return role1; - if (roleId === 2) return role2; - if (roleId === 3) return role3; + if (roleId === 0) return devBar; + if (roleId === 1) return navBar; + if (roleId === 2) return role1; + if (roleId === 3) return role2; + if (roleId === 4) return role3; return []; } \ No newline at end of file diff --git a/src/app/admin/(dashboard)/user&role/menu-access/page.tsx b/src/app/admin/(dashboard)/user&role/menu-access/page.tsx index da5e4c24..b31de9fd 100644 --- a/src/app/admin/(dashboard)/user&role/menu-access/page.tsx +++ b/src/app/admin/(dashboard)/user&role/menu-access/page.tsx @@ -8,6 +8,7 @@ import { Button, Checkbox, Group, Paper, Select, Stack, Text, Title } from '@man import { useEffect, useState } from 'react' import { useProxy } from 'valtio/utils' import user from '../../_state/user/user-state' +import { useShallowEffect } from '@mantine/hooks' // ✅ Helper: ekstrak semua menu ID dari struktur navBar @@ -23,6 +24,10 @@ function MenuAccessPage() { const [selectedUserId, setSelectedUserId] = useState(null) const [userMenus, setUserMenus] = useState([]) + useShallowEffect(() => { + stateUser.findMany.load() + }, []) + // ✅ Gunakan helper untuk ekstrak menu const availableMenus = extractMenuIds(navBar); diff --git a/src/app/admin/(dashboard)/user&role/user/page.tsx b/src/app/admin/(dashboard)/user&role/user/page.tsx index 6f1ecbc2..5f133409 100644 --- a/src/app/admin/(dashboard)/user&role/user/page.tsx +++ b/src/app/admin/(dashboard)/user&role/user/page.tsx @@ -8,6 +8,7 @@ import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../_com/header'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import user from '../../_state/user/user-state'; +import { authStore } from '@/store/authStore'; function User() { const [search, setSearch] = useState(""); @@ -95,24 +96,17 @@ function ListUser({ search }: { search: string }) { }); if (success) { - // Cek apakah role berubah - const res = await fetch('/api/user/updt', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id: userId, - roleId: newRoleId, - }), - }); - const data = await res.json(); - - if (data.roleChanged) { - // Tampilkan notifikasi - alert(`User ${username} akan logout otomatis!`); + // ✅ Logout user jika sedang mengedit diri sendiri + const currentUserId = authStore.user?.id; + if (currentUserId === userId) { + authStore.setUser(null); + document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`; + alert("Perubahan memerlukan login ulang"); + window.location.href = "/login"; + return; } + // Reload data stateUser.findMany.load(page, 10, search); } @@ -127,6 +121,17 @@ function ListUser({ search }: { search: string }) { }); if (success) { + // ✅ Logout user jika sedang mengedit diri sendiri + const currentUserId = authStore.user?.id; + if (currentUserId === userId) { + authStore.setUser(null); + document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`; + alert("Perubahan memerlukan login ulang"); + window.location.href = "/login"; + return; + } + + // Reload data stateUser.findMany.load(page, 10, search); } }; diff --git a/src/app/admin/_com/list_PageAdmin.tsx b/src/app/admin/_com/list_PageAdmin.tsx index de13c942..c6208d81 100644 --- a/src/app/admin/_com/list_PageAdmin.tsx +++ b/src/app/admin/_com/list_PageAdmin.tsx @@ -1,3 +1,407 @@ +export const devBar = [ + { + 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: "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" + } + ] + }, + { + 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" + } + ] + }, + { + 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" + }, + { + id: "Menu Access", + name: "Menu Access", + path: "/admin/user&role/menu-access" + } + ] + } +] + export const navBar = [ { id: "Landing Page", diff --git a/src/app/api/[[...slugs]]/_lib/user/delUser.ts b/src/app/api/[[...slugs]]/_lib/user/delUser.ts index 9823dea6..9f83ec93 100644 --- a/src/app/api/[[...slugs]]/_lib/user/delUser.ts +++ b/src/app/api/[[...slugs]]/_lib/user/delUser.ts @@ -1,12 +1,12 @@ -// /api/user/delete.ts +// /api/user/delUser.ts import prisma from '@/lib/prisma'; import { Context } from 'elysia'; -export default async function userDelete(context: Context) { +export default async function userDeleteAccount(context: Context) { const { id } = context.params as { id: string }; try { - // Cek user dulu + // 1. Cek user dulu const existingUser = await prisma.user.findUnique({ where: { id }, }); @@ -18,15 +18,39 @@ export default async function userDelete(context: Context) { }; } - // Hard delete (hapus permanen) - const deletedUser = await prisma.user.delete({ - where: { id }, + // ✅ 2. Hapus SEMUA relasi dalam TRANSACTION + const result = await prisma.$transaction(async (tx) => { + // Hapus UserSession + const deletedSessions = await tx.userSession.deleteMany({ + where: { userId: id }, + }); + + // ✅ Hapus UserMenuAccess + const deletedMenuAccess = await tx.userMenuAccess.deleteMany({ + where: { userId: id }, + }); + + // ✅ Tambahkan relasi lain jika ada (contoh): + // await tx.userLog.deleteMany({ where: { userId: id } }); + // await tx.userNotification.deleteMany({ where: { userId: id } }); + // await tx.userToken.deleteMany({ where: { userId: id } }); + + // Hapus user + const deletedUser = await tx.user.delete({ + where: { id }, + }); + + return { + user: deletedUser, + sessionsDeleted: deletedSessions.count, + menuAccessDeleted: deletedMenuAccess.count, + }; }); return { success: true, - message: 'User berhasil dihapus permanen', - data: deletedUser, + message: `User berhasil dihapus permanen (${result.sessionsDeleted} session, ${result.menuAccessDeleted} menu access)`, + data: result, }; } catch (error) { console.error('Error delete user:', error); @@ -35,4 +59,4 @@ export default async function userDelete(context: Context) { message: 'Terjadi kesalahan saat menghapus user', }; } -} +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/user/index.ts b/src/app/api/[[...slugs]]/_lib/user/index.ts index 76b1a49f..5e6aa75c 100644 --- a/src/app/api/[[...slugs]]/_lib/user/index.ts +++ b/src/app/api/[[...slugs]]/_lib/user/index.ts @@ -5,6 +5,7 @@ import userFindMany from "./findMany"; import userFindUnique from "./findUnique"; import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts` import userUpdate from "./updt"; +import userDeleteAccount from "./delUser"; const User = new Elysia({ prefix: "/api/user" }) .get("/findMany", userFindMany) @@ -25,7 +26,7 @@ const User = new Elysia({ prefix: "/api/user" }) }) } ) - .put("/delUser/:id", userDelete, { + .delete("/delUser/:id", userDeleteAccount, { params: t.Object({ id: t.String(), }), diff --git a/src/app/api/[[...slugs]]/_lib/user/updt.ts b/src/app/api/[[...slugs]]/_lib/user/updt.ts index d69a6171..59ac9a50 100644 --- a/src/app/api/[[...slugs]]/_lib/user/updt.ts +++ b/src/app/api/[[...slugs]]/_lib/user/updt.ts @@ -1,171 +1,46 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// import prisma from "@/lib/prisma"; -// import { Context } from "elysia"; - -// export default async function userUpdate(context: Context) { -// try { -// const { id, isActive, roleId } = await context.body as { -// id: string, -// isActive?: boolean, -// roleId?: string -// }; - -// if (!id) { -// return { -// success: false, -// message: "ID user wajib ada", -// }; -// } - -// // 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 !== 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: 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); -// return { -// success: false, -// message: "Gagal mengupdate user: " + (e.message || "Unknown error"), -// }; -// } -// } - - /* eslint-disable @typescript-eslint/no-explicit-any */ import prisma from "@/lib/prisma"; import { Context } from "elysia"; - -// API update user (Elysia atau Next.js API Route) +// API update user export default async function userUpdate(context: Context) { try { - const { id, isActive, roleId } = await context.body as { - id: string, - isActive?: boolean, - roleId?: string + const { id, isActive, roleId } = (await context.body) as { + id: string; + isActive?: boolean; + roleId?: string; }; if (!id) { - return { - success: false, - message: "ID user wajib ada", - }; + return { success: false, message: "ID user wajib ada" }; } - // Cek apakah roleId valid + // Validasi role if (roleId) { - const cekRole = await prisma.role.findUnique({ where: { id: roleId } }); - if (!cekRole) { - return { - success: false, - message: "Role tidak ditemukan", - }; - } + const role = await prisma.role.findUnique({ where: { id: roleId } }); + if (!role) return { success: false, message: "Role tidak ditemukan" }; } - // Deteksi perubahan role - let isRoleChanged = false; - if (roleId) { - const currentUser = await prisma.user.findUnique({ - where: { id }, - select: { roleId: true } - }); - isRoleChanged = currentUser?.roleId !== roleId; + const currentUser = await prisma.user.findUnique({ + where: { id }, + select: { roleId: true, isActive: true } + }); + + if (!currentUser) { + return { success: false, message: "User tidak ditemukan" }; } - // ✅ UPDATE USER + INVALIDATE SESSION + const isRoleChanged = roleId && currentUser.roleId !== roleId; + const isActiveChanged = isActive !== undefined && currentUser.isActive !== isActive; + + // Update user const updatedUser = await prisma.user.update({ where: { id }, data: { ...(isActive !== undefined && { isActive }), ...(roleId && { roleId }), - // Force logout: set sessionInvalid = true - ...(isRoleChanged && { sessionInvalid: true }), + // Force logout: invalidate semua sesi + ...(isRoleChanged || isActiveChanged ? { sessionInvalid: true } : {}), }, select: { id: true, @@ -177,26 +52,21 @@ export default async function userUpdate(context: Context) { } }); - // ✅ Reset sessionInvalid setelah 5 detik (opsional) - if (isRoleChanged) { - setTimeout(async () => { - try { - await prisma.user.update({ - where: { id }, - data: { sessionInvalid: false } - }); - } catch (e) { - console.error('Gagal reset sessionInvalid:', e); - } - }, 5000); + // ✅ HAPUS SEMUA SESI USER DI DATABASE + if (isRoleChanged || isActiveChanged) { + await prisma.userSession.deleteMany({ where: { userId: id } }); } return { success: true, - message: isRoleChanged - ? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.` - : "User berhasil diupdate", + roleChanged: isRoleChanged, + isActiveChanged, data: updatedUser, + message: isRoleChanged + ? `Role ${updatedUser.username} diubah. User akan logout otomatis.` + : isActiveChanged + ? `${updatedUser.username} ${isActive ? 'diaktifkan' : 'dinonaktifkan'}.` + : "User berhasil diupdate" }; } catch (e: any) { console.error("❌ Error update user:", e); diff --git a/src/app/api/auth/_lib/api_fetch_auth.ts b/src/app/api/auth/_lib/api_fetch_auth.ts index 794de057..19de8fc1 100644 --- a/src/app/api/auth/_lib/api_fetch_auth.ts +++ b/src/app/api/auth/_lib/api_fetch_auth.ts @@ -86,32 +86,14 @@ 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', { +// Ganti endpoint ke verify-otp-login +export const apiFetchVerifyOtp = async ({ nomor, otp, kodeId }: { nomor: string; otp: string; kodeId: string }) => { + const response = await fetch('/api/auth/verify-otp-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nomor, otp, kodeId }), }); - const data = await response.json(); - - // ✅ Jangan throw error untuk status 4xx — biarkan frontend handle return { success: response.ok, ...data, diff --git a/src/app/api/auth/_lib/session_create.ts b/src/app/api/auth/_lib/session_create.ts index 1a8b56f0..8a7c0692 100644 --- a/src/app/api/auth/_lib/session_create.ts +++ b/src/app/api/auth/_lib/session_create.ts @@ -1,7 +1,6 @@ // 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, @@ -14,78 +13,30 @@ export async function sessionCreate({ jwtSecret: string; user: Record; }) { - // 🔒 Validasi kunci tidak kosong + // ✅ Validasi env vars if (!sessionKey || sessionKey.length === 0) { throw new Error("sessionKey tidak boleh kosong"); } - if (!jwtSecret || jwtSecret.length === 0) { - throw new Error("jwtSecret tidak boleh kosong"); + if (!jwtSecret || jwtSecret.length < 32) { + throw new Error("jwtSecret minimal 32 karakter"); } - const token = await encrypt({ - exp, - jwtSecret, - user, - }); - - if (token === null) { + const token = await encrypt({ exp, jwtSecret, user }); + if (!token) { 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, { + (await cookies()).set(sessionKey, token, { httpOnly: true, sameSite: "lax", path: "/", secure: process.env.NODE_ENV === "production", - maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik + maxAge: 30 * 24 * 60 * 60, }); + console.log("✅ BASE_SESSION_KEY loaded:", !!process.env.BASE_SESSION_KEY); + console.log("✅ BASE_TOKEN_KEY loaded:", !!process.env.BASE_TOKEN_KEY); + return token; -} \ No newline at end of file +} diff --git a/src/app/api/auth/_lib/session_verify.ts b/src/app/api/auth/_lib/session_verify.ts index 2e529e1e..8b799a34 100644 --- a/src/app/api/auth/_lib/session_verify.ts +++ b/src/app/api/auth/_lib/session_verify.ts @@ -1,134 +1,43 @@ -// // 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 | 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; -// } -// } - -// src/app/api/auth/_lib/session_verify.ts -import { cookies } from 'next/headers'; -import { decrypt } from './decrypt'; -import prisma from '@/lib/prisma'; +// app/api/auth/_lib/session_verify.ts +import { cookies } from "next/headers"; +import { decrypt } from "./decrypt"; +import prisma from "@/lib/prisma"; export async function verifySession() { try { const sessionKey = process.env.BASE_SESSION_KEY; const jwtSecret = process.env.BASE_TOKEN_KEY; - - if (!sessionKey || !jwtSecret) { - throw new Error('Environment variables tidak lengkap'); - } + if (!sessionKey || !jwtSecret) throw new Error('Env tidak lengkap'); const token = (await cookies()).get(sessionKey)?.value; if (!token) return null; - // Decrypt JWT const jwtUser = await decrypt({ token, jwtSecret }); - if (!jwtUser || !jwtUser.id) return null; + if (!jwtUser?.id) return null; - // ✅ Cek apakah session di-invalidate - const user = await prisma.user.findUnique({ - where: { id: jwtUser.id as string }, - select: { - id: true, - username: true, - nomor: true, - roleId: true, - isActive: true, - sessionInvalid: true, // ← Tambahkan field ini + // Cari session di DB berdasarkan token + const dbSession = await prisma.userSession.findFirst({ + where: { + token, + active: true, + expiresAt: { gte: new Date() } }, + include: { user: true } }); - if (!user || user.sessionInvalid) { - console.log('⚠️ Session tidak valid (force logout)'); + if (!dbSession) { + console.log('⚠️ Session tidak ditemukan di DB'); return null; } - return user; + // ❌ Hanya tolak jika sessionInvalid = true + if (dbSession.user.sessionInvalid) { + console.log('⚠️ Session di-invalidate'); + return null; + } + + // ✅ Return user, meskipun isActive = false + return dbSession.user; } catch (error) { console.warn('Session verification failed:', error); return null; diff --git a/src/app/api/auth/finalize-registration/route.ts b/src/app/api/auth/finalize-registration/route.ts index 6fdc8f38..51890a8a 100644 --- a/src/app/api/auth/finalize-registration/route.ts +++ b/src/app/api/auth/finalize-registration/route.ts @@ -1,51 +1,64 @@ -// app/api/auth/finalize-registration/route.ts +// src/app/api/auth/finalize-registration/route.ts + import prisma from "@/lib/prisma"; import { NextResponse } from "next/server"; import { sessionCreate } from "../_lib/session_create"; export async function POST(req: Request) { try { - const { nomor, username, kodeId, roleId } = await req.json(); + const { nomor, username, kodeId } = await req.json(); - // Validasi input - if (!nomor || !username || !kodeId) { + const cleanNomor = nomor.replace(/\D/g, ""); + + if (!cleanNomor || !username || !kodeId) { return NextResponse.json( { success: false, message: "Data tidak lengkap" }, { status: 400 } ); } - // Verifikasi OTP + // Di awal fungsi POST + console.log("📦 Received payload:", { nomor, username, kodeId }); + + // Validasi OTP const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId }, }); - - if (!otpRecord?.isActive || otpRecord.nomor !== nomor) { + if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) { 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) { + // Cek duplikat username + if (await prisma.user.findFirst({ where: { username } })) { return NextResponse.json( { success: false, message: "Username sudah digunakan" }, - { status: 400 } + { status: 409 } ); } - // Buat user baru + // ✅ Gunakan username dari input user + const defaultRole = await prisma.role.findFirst({ + where: { name: "ADMIN DESA" }, + select: { id: true }, + }); + + if (!defaultRole) { + return NextResponse.json( + { success: false, message: "Role default tidak ditemukan" }, + { status: 500 } + ); + } + + // ✅ Buat user dengan username yang diinput const newUser = await prisma.user.create({ data: { - username, + username, // ✅ Ini yang benar nomor, - roleId: roleId || "1", // Default role - isActive: false, // Menunggu approval + roleId: defaultRole.id, + isActive: false, }, }); @@ -55,29 +68,22 @@ export async function POST(req: Request) { 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 } - ); - } + // ✅ BUAT SESI untuk user baru (meski isActive = false) + const token = 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, // ✅ Pastikan sesuai + roleId: newUser.roleId, + isActive: false, + }, + }); - return NextResponse.json({ + // Set cookie + const response = NextResponse.json({ success: true, message: "Registrasi berhasil. Menunggu persetujuan admin.", user: { @@ -88,6 +94,14 @@ export async function POST(req: Request) { }, }); + response.cookies.set(process.env.BASE_SESSION_KEY!, token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 30 * 24 * 60 * 60, + }); + + return response; } catch (error) { console.error("❌ Finalize Registration Error:", error); return NextResponse.json( @@ -97,4 +111,4 @@ export async function POST(req: Request) { } finally { await prisma.$disconnect(); } -} \ No newline at end of file +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index 2f96b589..18165deb 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -5,36 +5,54 @@ import prisma from '@/lib/prisma'; export async function GET() { try { - const user = await verifySession(); - if (!user) { + const sessionUser = await verifySession(); + if (!sessionUser) { return NextResponse.json( - { success: false, message: "Session tidak valid", user: null }, + { success: false, message: "Unauthorized", user: null }, { status: 401 } ); } - // ✅ Ambil menu akses kustom - const menuAccess = await prisma.userMenuAccess.findMany({ - where: { userId: user.id }, - select: { menuId: true }, - }); + const [dbUser, menuAccess] = await Promise.all([ + prisma.user.findUnique({ + where: { id: sessionUser.id }, + select: { + id: true, + username: true, + nomor: true, + roleId: true, // STRING! + isActive: true, // BOOLEAN! + }, + }), + prisma.userMenuAccess.findMany({ + where: { userId: sessionUser.id }, + select: { menuId: true }, + }), + ]); + + if (!dbUser) { + return NextResponse.json( + { success: false, message: "User not found", user: null }, + { status: 404 } + ); + } return NextResponse.json({ success: true, user: { - id: user.id, - name: user.username, - username: user.username, - nomor: user.nomor, - roleId: user.roleId, - isActive: user.isActive, - menuIds: menuAccess.map(m => m.menuId), // ✅ tambahkan ini + id: dbUser.id, + name: dbUser.username, + username: dbUser.username, + nomor: dbUser.nomor, + roleId: dbUser.roleId, // STRING! + isActive: dbUser.isActive, // BOOLEAN! + menuIds: menuAccess.map(m => m.menuId), }, }); } catch (error) { console.error("❌ Error in /api/auth/me:", error); return NextResponse.json( - { success: false, message: "Terjadi kesalahan", user: null }, + { success: false, message: "Internal server error", user: null }, { status: 500 } ); } diff --git a/src/app/api/auth/otp-data/route.ts b/src/app/api/auth/otp-data/route.ts index f1d6aef5..81b1c8c9 100644 --- a/src/app/api/auth/otp-data/route.ts +++ b/src/app/api/auth/otp-data/route.ts @@ -2,9 +2,10 @@ import { NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -export async function POST(req: Request) { +export async function GET(request: Request) { try { - const { kodeId } = await req.json(); + const { searchParams } = new URL(request.url); + const kodeId = searchParams.get("kodeId"); if (!kodeId) { return NextResponse.json( @@ -15,7 +16,7 @@ export async function POST(req: Request) { const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId }, - select: { id: true, nomor: true, isActive: true, createdAt: true }, + select: { nomor: true, isActive: true }, }); if (!otpRecord || !otpRecord.isActive) { @@ -27,12 +28,12 @@ export async function POST(req: Request) { return NextResponse.json({ success: true, - data: otpRecord, + data: { nomor: otpRecord.nomor }, }); } catch (error) { - console.error("Error fetching OTP data:", error); + console.error("❌ Gagal mengambil data OTP:", error); return NextResponse.json( - { success: false, message: "Gagal mengambil data OTP" }, + { success: false, message: "Terjadi kesalahan internal" }, { status: 500 } ); } finally { diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 8dbe79c6..ff87c8e3 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -14,7 +14,7 @@ export async function POST(req: Request) { 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 } })) { + if (await prisma.user.findFirst({ where: { username } })) { return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 }); } diff --git a/src/app/api/auth/resend/route.ts b/src/app/api/auth/resend/route.ts index 06e147a3..dafb5e44 100644 --- a/src/app/api/auth/resend/route.ts +++ b/src/app/api/auth/resend/route.ts @@ -1,71 +1,58 @@ +// src/app/api/auth/resend-otp/route.ts import prisma from "@/lib/prisma"; -import { randomOTP } from "../_lib/randomOTP"; + import { NextResponse } from "next/server"; +import { randomOTP } from "../_lib/randomOTP"; export async function POST(req: Request) { - if (req.method !== "POST") { - return NextResponse.json( - { success: false, message: "Method Not Allowed" }, - { status: 405 } - ); - } - try { - const codeOtp = randomOTP(); - const body = await req.json(); - const { nomor } = body; + const { nomor } = await req.json(); - const res = await fetch( - `https://wa.wibudev.com/code?nom=${nomor}&text=HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya. - \n - >> Kode OTP anda: ${codeOtp}. - ` - ); - - const sendWa = await res.json(); - if (sendWa.status !== "success") + if (!nomor || typeof nomor !== 'string') { return NextResponse.json( - { - success: false, - message: "Nomor Whatsapp Tidak Aktif", - }, + { success: false, message: "Nomor tidak valid" }, { status: 400 } ); + } - const createOtpId = await prisma.kodeOtp.create({ + const codeOtp = randomOTP(); + const otpNumber = Number(codeOtp); + + // Kirim OTP via WhatsApp + const waMessage = `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 } + ); + } + + // Simpan OTP ke database + const otpRecord = await prisma.kodeOtp.create({ data: { - nomor: nomor, - otp: codeOtp, + nomor, + otp: otpNumber, + isActive: true, }, }); - if (!createOtpId) - return NextResponse.json( - { - success: false, - message: "Gagal Membuat Kode OTP", - }, - { status: 400 } - ); + return NextResponse.json({ + success: true, + message: "OTP baru dikirim", + kodeId: otpRecord.id, + }); - return NextResponse.json( - { - success: true, - message: "Kode Verifikasi Dikirim", - kodeId: createOtpId.id, - }, - { status: 200 } - ); } catch (error) { - console.error(" Error Resend OTP", error); + console.error("Error Resend OTP:", error); return NextResponse.json( - { - success: false, - message: "Server Whatsapp Error !!", - }, + { success: false, message: "Gagal mengirim ulang OTP" }, { status: 500 } ); } finally { await prisma.$disconnect(); } -} +} \ No newline at end of file diff --git a/src/app/api/auth/send-otp-register/route.ts b/src/app/api/auth/send-otp-register/route.ts index 8c52cbf9..501ea316 100644 --- a/src/app/api/auth/send-otp-register/route.ts +++ b/src/app/api/auth/send-otp-register/route.ts @@ -14,7 +14,7 @@ export async function POST(req: Request) { 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 } })) { + if (await prisma.user.findFirst({ where: { username } })) { return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 }); } diff --git a/src/app/api/auth/verify-otp-login/route.ts b/src/app/api/auth/verify-otp-login/route.ts new file mode 100644 index 00000000..bc468297 --- /dev/null +++ b/src/app/api/auth/verify-otp-login/route.ts @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// src/app/api/auth/verify-otp-login/route.ts +import prisma from "@/lib/prisma"; +import { NextResponse } from "next/server"; +import { sessionCreate } from "../_lib/session_create"; + +export async function POST(req: Request) { + try { + const { nomor, otp, kodeId } = await req.json(); + + if (!nomor || !otp || !kodeId) { + return NextResponse.json( + { success: false, message: "Data tidak lengkap" }, + { status: 400 } + ); + } + + const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } }); + if (!otpRecord || !otpRecord.isActive || otpRecord.nomor !== nomor) { + return NextResponse.json( + { success: false, message: "Kode verifikasi tidak valid" }, + { status: 400 } + ); + } + + const receivedOtp = Number(otp); + if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) { + return NextResponse.json( + { success: false, message: "Kode OTP salah" }, + { status: 400 } + ); + } + + // 🔍 CARI USER — JANGAN BUAT BARU! + const user = await prisma.user.findUnique({ + where: { nomor }, + select: { id: true, nomor: true, username: true, roleId: true, isActive: true }, + }); + + if (!user) { + // ❌ Nomor belum terdaftar → suruh registrasi + return NextResponse.json( + { success: false, message: "Akun tidak ditemukan. Silakan registrasi terlebih dahulu." }, + { status: 404 } + ); + } + + // ✅ Buat sesi + const token = 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, + }, + }); + + await prisma.$transaction([ + prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } }), + prisma.user.update({ where: { id: user.id }, data: { lastLogin: new Date() } }), + ]); + + const response = NextResponse.json({ + success: true, + message: user.isActive ? "Berhasil login" : "Menunggu persetujuan", + user: { + id: user.id, + name: user.username, + roleId: user.roleId, + isActive: user.isActive, + }, + }); + + response.cookies.set(process.env.BASE_SESSION_KEY!, token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 30 * 24 * 60 * 60, + }); + + return response; + } catch (error: any) { + console.error("❌ Verify OTP Login Error:", error); + if (error.message.includes("sessionKey") || error.message.includes("jwtSecret")) { + return NextResponse.json( + { success: false, message: "Konfigurasi server tidak lengkap" }, + { status: 500 } + ); + } + return NextResponse.json( + { success: false, message: "Terjadi kesalahan saat login" }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/verify-otp-register/route.ts b/src/app/api/auth/verify-otp-register/route.ts new file mode 100644 index 00000000..66cbf2d6 --- /dev/null +++ b/src/app/api/auth/verify-otp-register/route.ts @@ -0,0 +1,61 @@ +// src/app/api/auth/verify-otp-register/route.ts +import prisma from "@/lib/prisma"; +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + try { + const { nomor, otp, kodeId } = await req.json(); + + if (!nomor || !otp || !kodeId) { + return NextResponse.json( + { success: false, message: "Data tidak lengkap" }, + { status: 400 } + ); + } + + const otpRecord = await prisma.kodeOtp.findUnique({ + where: { id: kodeId }, + }); + + if (!otpRecord || !otpRecord.isActive) { + return NextResponse.json( + { success: false, message: "Kode verifikasi tidak valid atau sudah kadaluarsa" }, + { status: 400 } + ); + } + + if (otpRecord.nomor !== nomor) { + return NextResponse.json( + { success: false, message: "Nomor tidak sesuai dengan kode verifikasi" }, + { status: 400 } + ); + } + + const receivedOtp = Number(otp); + if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) { + return NextResponse.json( + { success: false, message: "Kode OTP salah" }, + { status: 400 } + ); + } + + // ✅ Hanya validasi — jangan update isActive! + return NextResponse.json({ + success: true, + message: "OTP valid. Lanjutkan ke finalisasi registrasi.", + data: { + nomor, + kodeId, + }, + }); + + } catch (error) { + console.error("❌ Verify OTP Register Error:", error); + return NextResponse.json( + { success: false, message: "Terjadi kesalahan saat verifikasi OTP" }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts deleted file mode 100644 index c14fb87e..00000000 --- a/src/app/api/auth/verify-otp/route.ts +++ /dev/null @@ -1,126 +0,0 @@ -// app/api/auth/verify-otp/route.ts -import prisma from "@/lib/prisma"; -import { NextResponse } from "next/server"; -import { sessionCreate } from "../_lib/session_create"; - -export async function POST(req: Request) { - try { - const { nomor, otp, kodeId } = await req.json(); - - // Validasi input - if (!nomor || !otp || !kodeId) { - return NextResponse.json( - { success: false, message: "Data tidak lengkap" }, - { 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 digunakan" }, - { status: 400 } - ); - } - - // Validasi OTP - const receivedOtp = Number(otp); - if (isNaN(receivedOtp) || 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 user berdasarkan nomor - const user = await prisma.user.findUnique({ - where: { nomor }, - select: { - id: true, - nomor: true, - username: true, - roleId: true, - isActive: true, - }, - }); - - if (!user) { - return NextResponse.json( - { success: false, message: "Akun tidak ditemukan" }, - { status: 404 } - ); - } - - // ✅ 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 } - ); - } - - // Nonaktifkan OTP - await prisma.kodeOtp.update({ - where: { id: kodeId }, - data: { isActive: false }, - }); - - // 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, - }, - }); - - } catch (error) { - console.error("❌ Verify OTP Error:", error); - return NextResponse.json( - { success: false, message: "Terjadi kesalahan saat verifikasi" }, - { status: 500 } - ); - } finally { - await prisma.$disconnect(); - } -} \ No newline at end of file diff --git a/src/app/waiting-room/page.tsx b/src/app/waiting-room/page.tsx index e5b25bbb..fe8841f6 100644 --- a/src/app/waiting-room/page.tsx +++ b/src/app/waiting-room/page.tsx @@ -6,55 +6,77 @@ import { Center, Loader, Paper, Stack, Text, Title } from '@mantine/core'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -// Ganti ini jika tidak pakai next-auth async function fetchUser() { const res = await fetch('/api/auth/me'); - if (!res.ok) throw new Error('Unauthorized'); + if (!res.ok) { + // Jangan throw error — biarkan handle status code + const text = await res.text(); + throw new Error(`HTTP ${res.status}: ${text}`); + } return res.json(); } export default function WaitingRoom() { const router = useRouter(); const [user, setUser] = useState(null); - // const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [isRedirecting, setIsRedirecting] = useState(false); + useEffect(() => { + let isMounted = true; + const interval = setInterval(async () => { + if (isRedirecting || !isMounted) return; -useEffect(() => { - let isMounted = true; - const interval = setInterval(async () => { - try { - const data = await fetchUser(); - if (!isMounted) return; + try { + const data = await fetchUser(); + if (!isMounted) return; - const currentUser = data.user; - setUser(currentUser); + const currentUser = data.user; + setUser(currentUser); - // ✅ Sekarang isActive tersedia! - if (currentUser?.isActive) { - clearInterval(interval); - // Redirect ke halaman admin sesuai role - if (currentUser.roleId === 0) { - router.push('/admin/landing-page/profil/program-inovasi'); + // ✅ Periksa isActive dan redirect + if (currentUser?.isActive === true) { + setIsRedirecting(true); + clearInterval(interval); + + // ✅ roleId adalah STRING → gunakan string literal + let redirectPath = '/admin'; + + switch (currentUser.roleId) { + case "0": // DEVELOPER + case "1": // SUPERADMIN + case "2": // ADMIN_DESA + redirectPath = '/admin/landing-page/profil/program-inovasi'; + break; + case "3": // ADMIN_KESEHATAN + redirectPath = '/admin/kesehatan/posyandu'; + break; + case "4": // ADMIN_PENDIDIKAN + redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan'; + break; + } + + setTimeout(() => router.push(redirectPath), 500); + } + } catch (err: any) { + if (!isMounted) return; + + // ❌ Hanya redirect ke /login jika benar-benar tidak ada sesi + if (err.message.includes('401')) { + setError('Sesi tidak valid'); + clearInterval(interval); + router.push('/login'); } else { - router.push('/admin'); // atau halaman default role + console.error('Error polling:', err); } } - } catch (err: any) { - if (!isMounted) return; - setError(err.message || 'Gagal memuat status'); - clearInterval(interval); - if (err.message === 'Unauthorized') { - router.push('/login'); - } - } - }, 2000); + }, 3000); - return () => { - isMounted = false; - clearInterval(interval); - }; -}, [router]); + return () => { + isMounted = false; + clearInterval(interval); + }; + }, [router, isRedirecting]); if (error) { return ( @@ -69,6 +91,24 @@ useEffect(() => { ); } + if (isRedirecting) { + return ( +
+ + + + Akun Disetujui! ✅ + + + Mengalihkan ke dashboard... + + + + +
+ ); + } + return (
@@ -76,17 +116,13 @@ useEffect(() => { Menunggu Persetujuan - Akun Anda sedang dalam proses verifikasi oleh Superadmin. - Nomor: {user?.nomor || '...'} - - Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.