Fix Kondisi Verify Otp Registrasi dan Login

Next mau fix eror saat user sudah terdaftar tetapi di redirect ke login, seharusnya redirect sesuai roleIdnya
This commit is contained in:
2025-11-25 15:03:27 +08:00
parent 716db0adca
commit ace5aff1b6
24 changed files with 1069 additions and 788 deletions

View File

@@ -1,24 +1,30 @@
[ [
{ {
"id": "0", "id": "0",
"name": "DEVELOPER",
"description": "Developer",
"isActive": true
},
{
"id": "1",
"name": "SUPER ADMIN", "name": "SUPER ADMIN",
"description": "Administrator", "description": "Administrator",
"isActive": true "isActive": true
}, },
{ {
"id": "1", "id": "2",
"name": "ADMIN DESA", "name": "ADMIN DESA",
"description": "Administrator Desa", "description": "Administrator Desa",
"isActive": true "isActive": true
}, },
{ {
"id": "2", "id": "3",
"name": "ADMIN KESEHATAN", "name": "ADMIN KESEHATAN",
"description": "Administrator Bidang Kesehatan", "description": "Administrator Bidang Kesehatan",
"isActive": true "isActive": true
}, },
{ {
"id": "3", "id": "4",
"name": "ADMIN PENDIDIKAN", "name": "ADMIN PENDIDIKAN",
"description": "Administrator Bidang Pendidikan", "description": "Administrator Bidang Pendidikan",
"isActive": true "isActive": true

View File

@@ -2163,20 +2163,21 @@ enum StatusPeminjaman {
// ========================================= USER ========================================= // // ========================================= USER ========================================= //
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
username String @unique username String
nomor String @unique nomor String @unique
role Role @relation(fields: [roleId], references: [id]) roleId String @default("2")
roleId String @default("1") isActive Boolean @default(false)
instansi String? sessionInvalid Boolean @default(false)
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
isActive Boolean @default(true)
lastLogin DateTime? lastLogin DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @default(now()) @updatedAt
deletedAt DateTime?
sessionInvalid Boolean @default(false) sessions UserSession[] // ✅ Relasi one-to-many
UserMenuAccess UserMenuAccess[] role Role @relation(fields: [roleId], references: [id])
menuAccesses UserMenuAccess[]
@@map("users")
} }
model Role { model Role {
@@ -2203,13 +2204,18 @@ model KodeOtp {
model UserSession { model UserSession {
id String @id @default(cuid()) id String @id @default(cuid())
token String token String @db.Text // ✅ JWT bisa panjang
expires DateTime? expiresAt DateTime // ✅ Ubah jadi expiresAt (konsisten)
active Boolean @default(true) active Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt 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 { model UserMenuAccess {

View File

@@ -118,7 +118,7 @@ const userState = proxy({
console.error("Gagal delete user:", error); console.error("Gagal delete user:", error);
toast.error("Terjadi kesalahan saat menghapus user"); toast.error("Terjadi kesalahan saat menghapus user");
} finally { } finally {
userState.delete.loading = false; userState.deleteUser.loading = false;
} }
}, },
}, },

View File

@@ -1,10 +1,17 @@
//
'use client'; 'use client';
import { apiFetchOtpData, apiFetchVerifyOtp } from '@/app/api/auth/_lib/api_fetch_auth';
import colors from '@/con/colors'; 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 { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -17,8 +24,14 @@ export default function Validasi() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [kodeId, setKodeId] = useState<string | null>(null); const [kodeId, setKodeId] = useState<string | null>(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(() => { useEffect(() => {
const storedKodeId = localStorage.getItem('auth_kodeId'); const storedKodeId = localStorage.getItem('auth_kodeId');
if (!storedKodeId) { if (!storedKodeId) {
@@ -28,11 +41,12 @@ export default function Validasi() {
} }
setKodeId(storedKodeId); setKodeId(storedKodeId);
const loadOtpData = async () => { const loadOtpData = async () => {
try { try {
const result = await apiFetchOtpData({ kodeId: storedKodeId }); const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
if (result.success && result.data?.nomor) { const result = await res.json();
if (res.ok && result.data?.nomor) {
setNomor(result.data.nomor); setNomor(result.data.nomor);
} else { } else {
throw new Error('Data OTP tidak valid'); throw new Error('Data OTP tidak valid');
@@ -45,82 +59,21 @@ export default function Validasi() {
setIsLoading(false); setIsLoading(false);
} }
}; };
loadOtpData(); loadOtpData();
}, [router]); }, [router]);
// Verifikasi OTP
const handleVerify = async () => { const handleVerify = async () => {
if (!kodeId || !nomor || otp.length < 4) return; if (!kodeId || !nomor || otp.length < 4) return;
setLoading(true); setLoading(true);
try { try {
const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId }); if (isRegistrationFlow) {
// 🔑 Alur REGISTRASI
if (!verifyResult.success) { await handleRegistrationVerification();
// Registrasi baru? } else {
if ( // 🔑 Alur LOGIN
verifyResult.status === 404 && await handleLoginVerification();
verifyResult.message?.includes('Akun tidak ditemukan')
) {
await handleNewRegistration();
return;
}
// 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) { } catch (error) {
console.error('Error saat verifikasi:', error); console.error('Error saat verifikasi:', error);
toast.error('Terjadi kesalahan sistem'); toast.error('Terjadi kesalahan sistem');
@@ -129,38 +82,111 @@ export default function Validasi() {
} }
}; };
// Registrasi baru // ✅ Verifikasi OTP untuk REGISTRASI
const handleNewRegistration = async () => { const handleRegistrationVerification = async () => {
const username = localStorage.getItem('auth_username'); const username = localStorage.getItem('auth_username');
if (!username) { if (!username) {
toast.error('Data registrasi tidak ditemukan'); toast.error('Data registrasi tidak ditemukan. Silakan ulangi dari awal.');
return; return;
} }
try { // ✅ Validasi format
const res = await fetch('/api/auth/finalize-registration', { const cleanNomor = nomor?.replace(/\D/g, '') ?? '';
method: 'POST', if (cleanNomor.length < 10) {
headers: { 'Content-Type': 'application/json' }, toast.error('Nomor tidak valid');
body: JSON.stringify({ nomor, username, otp, kodeId }), return;
}); }
const data = await res.json(); if (username.trim().length < 5) {
toast.error('Username minimal 5 karakter');
return;
}
if (data.success) { // 1. Verifikasi OTP via endpoint register
// Set user sementara (tanpa roleId, akan diisi saat approve) const verifyRes = await fetch('/api/auth/verify-otp-register', {
authStore.setUser({ method: 'POST',
id: 'pending', headers: { 'Content-Type': 'application/json' },
name: username, body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
roleId: 1, });
});
cleanupStorage(); const verifyData = await verifyRes.json();
router.replace('/waiting-room');
} else { if (!verifyRes.ok) {
toast.error(data.message || 'Registrasi gagal'); toast.error(verifyData.message || 'Verifikasi OTP gagal');
} return;
} catch (error) { }
console.error('Error registrasi:', error);
toast.error('Gagal menyelesaikan registrasi'); // 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 () => { const handleResend = async () => {
if (!nomor) return; if (!nomor) return;
try { try {
const res = await fetch('/api/auth/resend-otp', { const res = await fetch('/api/auth/resend', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nomor }), body: JSON.stringify({ nomor }),
@@ -190,7 +216,6 @@ export default function Validasi() {
} }
}; };
// Loading
if (isLoading) { if (isLoading) {
return ( return (
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh"> <Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
@@ -209,7 +234,7 @@ export default function Validasi() {
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Box> <Box>
<Title ta="center" order={2} fw="bold" c={colors['blue-button']}> <Title ta="center" order={2} fw="bold" c={colors['blue-button']}>
Kode Verifikasi {isRegistrationFlow ? 'Verifikasi Registrasi' : 'Verifikasi Login'}
</Title> </Title>
<Text ta="center" size="sm" c="dimmed" mt="xs"> <Text ta="center" size="sm" c="dimmed" mt="xs">
Kami telah mengirim kode ke nomor <strong>{nomor}</strong> Kami telah mengirim kode ke nomor <strong>{nomor}</strong>

View File

@@ -1,5 +1,5 @@
// src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts // 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({ export function getNavbar({
roleId, roleId,
@@ -14,9 +14,10 @@ export function getNavbar({
} }
// Fallback ke role-based // Fallback ke role-based
if (roleId === 0) return navBar; if (roleId === 0) return devBar;
if (roleId === 1) return role1; if (roleId === 1) return navBar;
if (roleId === 2) return role2; if (roleId === 2) return role1;
if (roleId === 3) return role3; if (roleId === 3) return role2;
if (roleId === 4) return role3;
return []; return [];
} }

View File

@@ -8,6 +8,7 @@ import { Button, Checkbox, Group, Paper, Select, Stack, Text, Title } from '@man
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import user from '../../_state/user/user-state' import user from '../../_state/user/user-state'
import { useShallowEffect } from '@mantine/hooks'
// ✅ Helper: ekstrak semua menu ID dari struktur navBar // ✅ Helper: ekstrak semua menu ID dari struktur navBar
@@ -23,6 +24,10 @@ function MenuAccessPage() {
const [selectedUserId, setSelectedUserId] = useState<string | null>(null) const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
const [userMenus, setUserMenus] = useState<string[]>([]) const [userMenus, setUserMenus] = useState<string[]>([])
useShallowEffect(() => {
stateUser.findMany.load()
}, [])
// ✅ Gunakan helper untuk ekstrak menu // ✅ Gunakan helper untuk ekstrak menu
const availableMenus = extractMenuIds(navBar); const availableMenus = extractMenuIds(navBar);

View File

@@ -8,6 +8,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import user from '../../_state/user/user-state'; import user from '../../_state/user/user-state';
import { authStore } from '@/store/authStore';
function User() { function User() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -95,24 +96,17 @@ function ListUser({ search }: { search: string }) {
}); });
if (success) { if (success) {
// Cek apakah role berubah // ✅ Logout user jika sedang mengedit diri sendiri
const res = await fetch('/api/user/updt', { const currentUserId = authStore.user?.id;
method: 'POST', if (currentUserId === userId) {
headers: { authStore.setUser(null);
'Content-Type': 'application/json', document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`;
}, alert("Perubahan memerlukan login ulang");
body: JSON.stringify({ window.location.href = "/login";
id: userId, return;
roleId: newRoleId,
}),
});
const data = await res.json();
if (data.roleChanged) {
// Tampilkan notifikasi
alert(`User ${username} akan logout otomatis!`);
} }
// Reload data
stateUser.findMany.load(page, 10, search); stateUser.findMany.load(page, 10, search);
} }
@@ -127,6 +121,17 @@ function ListUser({ search }: { search: string }) {
}); });
if (success) { 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); stateUser.findMany.load(page, 10, search);
} }
}; };

View File

@@ -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 = [ export const navBar = [
{ {
id: "Landing Page", id: "Landing Page",

View File

@@ -1,12 +1,12 @@
// /api/user/delete.ts // /api/user/delUser.ts
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { Context } from 'elysia'; 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 }; const { id } = context.params as { id: string };
try { try {
// Cek user dulu // 1. Cek user dulu
const existingUser = await prisma.user.findUnique({ const existingUser = await prisma.user.findUnique({
where: { id }, where: { id },
}); });
@@ -18,15 +18,39 @@ export default async function userDelete(context: Context) {
}; };
} }
// Hard delete (hapus permanen) // ✅ 2. Hapus SEMUA relasi dalam TRANSACTION
const deletedUser = await prisma.user.delete({ const result = await prisma.$transaction(async (tx) => {
where: { id }, // 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 { return {
success: true, success: true,
message: 'User berhasil dihapus permanen', message: `User berhasil dihapus permanen (${result.sessionsDeleted} session, ${result.menuAccessDeleted} menu access)`,
data: deletedUser, data: result,
}; };
} catch (error) { } catch (error) {
console.error('Error delete user:', error); console.error('Error delete user:', error);

View File

@@ -5,6 +5,7 @@ import userFindMany from "./findMany";
import userFindUnique from "./findUnique"; import userFindUnique from "./findUnique";
import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts` import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts`
import userUpdate from "./updt"; import userUpdate from "./updt";
import userDeleteAccount from "./delUser";
const User = new Elysia({ prefix: "/api/user" }) const User = new Elysia({ prefix: "/api/user" })
.get("/findMany", userFindMany) .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({ params: t.Object({
id: t.String(), id: t.String(),
}), }),

View File

@@ -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 */ /* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
// API update user
// API update user (Elysia atau Next.js API Route)
export default async function userUpdate(context: Context) { export default async function userUpdate(context: Context) {
try { try {
const { id, isActive, roleId } = await context.body as { const { id, isActive, roleId } = (await context.body) as {
id: string, id: string;
isActive?: boolean, isActive?: boolean;
roleId?: string roleId?: string;
}; };
if (!id) { if (!id) {
return { return { success: false, message: "ID user wajib ada" };
success: false,
message: "ID user wajib ada",
};
} }
// Cek apakah roleId valid // Validasi role
if (roleId) { if (roleId) {
const cekRole = await prisma.role.findUnique({ where: { id: roleId } }); const role = await prisma.role.findUnique({ where: { id: roleId } });
if (!cekRole) { if (!role) return { success: false, message: "Role tidak ditemukan" };
return {
success: false,
message: "Role tidak ditemukan",
};
}
} }
// Deteksi perubahan role const currentUser = await prisma.user.findUnique({
let isRoleChanged = false; where: { id },
if (roleId) { select: { roleId: true, isActive: true }
const currentUser = await prisma.user.findUnique({ });
where: { id },
select: { roleId: true } if (!currentUser) {
}); return { success: false, message: "User tidak ditemukan" };
isRoleChanged = currentUser?.roleId !== roleId;
} }
// ✅ 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({ const updatedUser = await prisma.user.update({
where: { id }, where: { id },
data: { data: {
...(isActive !== undefined && { isActive }), ...(isActive !== undefined && { isActive }),
...(roleId && { roleId }), ...(roleId && { roleId }),
// Force logout: set sessionInvalid = true // Force logout: invalidate semua sesi
...(isRoleChanged && { sessionInvalid: true }), ...(isRoleChanged || isActiveChanged ? { sessionInvalid: true } : {}),
}, },
select: { select: {
id: true, id: true,
@@ -177,26 +52,21 @@ export default async function userUpdate(context: Context) {
} }
}); });
// ✅ Reset sessionInvalid setelah 5 detik (opsional) // ✅ HAPUS SEMUA SESI USER DI DATABASE
if (isRoleChanged) { if (isRoleChanged || isActiveChanged) {
setTimeout(async () => { await prisma.userSession.deleteMany({ where: { userId: id } });
try {
await prisma.user.update({
where: { id },
data: { sessionInvalid: false }
});
} catch (e) {
console.error('Gagal reset sessionInvalid:', e);
}
}, 5000);
} }
return { return {
success: true, success: true,
message: isRoleChanged roleChanged: isRoleChanged,
? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.` isActiveChanged,
: "User berhasil diupdate",
data: updatedUser, data: updatedUser,
message: isRoleChanged
? `Role ${updatedUser.username} diubah. User akan logout otomatis.`
: isActiveChanged
? `${updatedUser.username} ${isActive ? 'diaktifkan' : 'dinonaktifkan'}.`
: "User berhasil diupdate"
}; };
} catch (e: any) { } catch (e: any) {
console.error("❌ Error update user:", e); console.error("❌ Error update user:", e);

View File

@@ -86,32 +86,14 @@ export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => {
return data; return data;
}; };
export const apiFetchVerifyOtp = async ({ // Ganti endpoint ke verify-otp-login
nomor, export const apiFetchVerifyOtp = async ({ nomor, otp, kodeId }: { nomor: string; otp: string; kodeId: string }) => {
otp, const response = await fetch('/api/auth/verify-otp-login', {
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', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nomor, otp, kodeId }), body: JSON.stringify({ nomor, otp, kodeId }),
}); });
const data = await response.json(); const data = await response.json();
// ✅ Jangan throw error untuk status 4xx — biarkan frontend handle
return { return {
success: response.ok, success: response.ok,
...data, ...data,

View File

@@ -1,7 +1,6 @@
// app/api/auth/_lib/session_create.ts // app/api/auth/_lib/session_create.ts
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { encrypt } from "./encrypt"; import { encrypt } from "./encrypt";
import prisma from "@/lib/prisma";
export async function sessionCreate({ export async function sessionCreate({
sessionKey, sessionKey,
@@ -14,78 +13,30 @@ export async function sessionCreate({
jwtSecret: string; jwtSecret: string;
user: Record<string, unknown>; user: Record<string, unknown>;
}) { }) {
// 🔒 Validasi kunci tidak kosong // Validasi env vars
if (!sessionKey || sessionKey.length === 0) { if (!sessionKey || sessionKey.length === 0) {
throw new Error("sessionKey tidak boleh kosong"); throw new Error("sessionKey tidak boleh kosong");
} }
if (!jwtSecret || jwtSecret.length === 0) { if (!jwtSecret || jwtSecret.length < 32) {
throw new Error("jwtSecret tidak boleh kosong"); throw new Error("jwtSecret minimal 32 karakter");
} }
const token = await encrypt({ const token = await encrypt({ exp, jwtSecret, user });
exp, if (!token) {
jwtSecret,
user,
});
if (token === null) {
throw new Error("Token generation failed"); 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 // Set cookie
const cookieStore = await cookies(); (await cookies()).set(sessionKey, token, {
cookieStore.set(sessionKey, token, {
httpOnly: true, httpOnly: true,
sameSite: "lax", sameSite: "lax",
path: "/", path: "/",
secure: process.env.NODE_ENV === "production", 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; return token;
} }

View File

@@ -1,134 +1,43 @@
// // app/api/auth/_lib/session_verify.ts // app/api/auth/_lib/session_verify.ts
// import { cookies } from 'next/headers'; import { cookies } from "next/headers";
// import { decrypt } from './decrypt'; import { decrypt } from "./decrypt";
// import prisma from '@/lib/prisma'; 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;
// }
// }
// src/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() { export async function verifySession() {
try { try {
const sessionKey = process.env.BASE_SESSION_KEY; const sessionKey = process.env.BASE_SESSION_KEY;
const jwtSecret = process.env.BASE_TOKEN_KEY; const jwtSecret = process.env.BASE_TOKEN_KEY;
if (!sessionKey || !jwtSecret) throw new Error('Env tidak lengkap');
if (!sessionKey || !jwtSecret) {
throw new Error('Environment variables tidak lengkap');
}
const token = (await cookies()).get(sessionKey)?.value; const token = (await cookies()).get(sessionKey)?.value;
if (!token) return null; if (!token) return null;
// Decrypt JWT
const jwtUser = await decrypt({ token, jwtSecret }); const jwtUser = await decrypt({ token, jwtSecret });
if (!jwtUser || !jwtUser.id) return null; if (!jwtUser?.id) return null;
// ✅ Cek apakah session di-invalidate // Cari session di DB berdasarkan token
const user = await prisma.user.findUnique({ const dbSession = await prisma.userSession.findFirst({
where: { id: jwtUser.id as string }, where: {
select: { token,
id: true, active: true,
username: true, expiresAt: { gte: new Date() }
nomor: true,
roleId: true,
isActive: true,
sessionInvalid: true, // ← Tambahkan field ini
}, },
include: { user: true }
}); });
if (!user || user.sessionInvalid) { if (!dbSession) {
console.log('⚠️ Session tidak valid (force logout)'); console.log('⚠️ Session tidak ditemukan di DB');
return null; 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) { } catch (error) {
console.warn('Session verification failed:', error); console.warn('Session verification failed:', error);
return null; return null;

View File

@@ -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 prisma from "@/lib/prisma";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { sessionCreate } from "../_lib/session_create"; import { sessionCreate } from "../_lib/session_create";
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const { nomor, username, kodeId, roleId } = await req.json(); const { nomor, username, kodeId } = await req.json();
// Validasi input const cleanNomor = nomor.replace(/\D/g, "");
if (!nomor || !username || !kodeId) {
if (!cleanNomor || !username || !kodeId) {
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Data tidak lengkap" }, { success: false, message: "Data tidak lengkap" },
{ status: 400 } { status: 400 }
); );
} }
// Verifikasi OTP // Di awal fungsi POST
console.log("📦 Received payload:", { nomor, username, kodeId });
// Validasi OTP
const otpRecord = await prisma.kodeOtp.findUnique({ const otpRecord = await prisma.kodeOtp.findUnique({
where: { id: kodeId }, where: { id: kodeId },
}); });
if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
if (!otpRecord?.isActive || otpRecord.nomor !== nomor) {
return NextResponse.json( return NextResponse.json(
{ success: false, message: "OTP tidak valid" }, { success: false, message: "OTP tidak valid" },
{ status: 400 } { status: 400 }
); );
} }
// Cek apakah username sudah dipakai // Cek duplikat username
const existingUser = await prisma.user.findUnique({ if (await prisma.user.findFirst({ where: { username } })) {
where: { username },
});
if (existingUser) {
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Username sudah digunakan" }, { 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({ const newUser = await prisma.user.create({
data: { data: {
username, username, // ✅ Ini yang benar
nomor, nomor,
roleId: roleId || "1", // Default role roleId: defaultRole.id,
isActive: false, // Menunggu approval isActive: false,
}, },
}); });
@@ -55,29 +68,22 @@ export async function POST(req: Request) {
data: { isActive: false }, data: { isActive: false },
}); });
// ✅ CREATE SESSION (JWT + Database) // ✅ BUAT SESI untuk user baru (meski isActive = false)
try { const token = await sessionCreate({
await sessionCreate({ sessionKey: process.env.BASE_SESSION_KEY!,
sessionKey: process.env.BASE_SESSION_KEY!, jwtSecret: process.env.BASE_TOKEN_KEY!,
jwtSecret: process.env.BASE_TOKEN_KEY!, exp: "30 day",
exp: "30 day", user: {
user: { id: newUser.id,
id: newUser.id, nomor: newUser.nomor,
nomor: newUser.nomor, username: newUser.username, // ✅ Pastikan sesuai
username: newUser.username, roleId: newUser.roleId,
roleId: newUser.roleId, isActive: false,
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({ // Set cookie
const response = NextResponse.json({
success: true, success: true,
message: "Registrasi berhasil. Menunggu persetujuan admin.", message: "Registrasi berhasil. Menunggu persetujuan admin.",
user: { 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) { } catch (error) {
console.error("❌ Finalize Registration Error:", error); console.error("❌ Finalize Registration Error:", error);
return NextResponse.json( return NextResponse.json(

View File

@@ -5,36 +5,54 @@ import prisma from '@/lib/prisma';
export async function GET() { export async function GET() {
try { try {
const user = await verifySession(); const sessionUser = await verifySession();
if (!user) { if (!sessionUser) {
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Session tidak valid", user: null }, { success: false, message: "Unauthorized", user: null },
{ status: 401 } { status: 401 }
); );
} }
// ✅ Ambil menu akses kustom const [dbUser, menuAccess] = await Promise.all([
const menuAccess = await prisma.userMenuAccess.findMany({ prisma.user.findUnique({
where: { userId: user.id }, where: { id: sessionUser.id },
select: { menuId: true }, 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({ return NextResponse.json({
success: true, success: true,
user: { user: {
id: user.id, id: dbUser.id,
name: user.username, name: dbUser.username,
username: user.username, username: dbUser.username,
nomor: user.nomor, nomor: dbUser.nomor,
roleId: user.roleId, roleId: dbUser.roleId, // STRING!
isActive: user.isActive, isActive: dbUser.isActive, // BOOLEAN!
menuIds: menuAccess.map(m => m.menuId), // ✅ tambahkan ini menuIds: menuAccess.map(m => m.menuId),
}, },
}); });
} catch (error) { } catch (error) {
console.error("❌ Error in /api/auth/me:", error); console.error("❌ Error in /api/auth/me:", error);
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Terjadi kesalahan", user: null }, { success: false, message: "Internal server error", user: null },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -2,9 +2,10 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
export async function POST(req: Request) { export async function GET(request: Request) {
try { try {
const { kodeId } = await req.json(); const { searchParams } = new URL(request.url);
const kodeId = searchParams.get("kodeId");
if (!kodeId) { if (!kodeId) {
return NextResponse.json( return NextResponse.json(
@@ -15,7 +16,7 @@ export async function POST(req: Request) {
const otpRecord = await prisma.kodeOtp.findUnique({ const otpRecord = await prisma.kodeOtp.findUnique({
where: { id: kodeId }, where: { id: kodeId },
select: { id: true, nomor: true, isActive: true, createdAt: true }, select: { nomor: true, isActive: true },
}); });
if (!otpRecord || !otpRecord.isActive) { if (!otpRecord || !otpRecord.isActive) {
@@ -27,12 +28,12 @@ export async function POST(req: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
data: otpRecord, data: { nomor: otpRecord.nomor },
}); });
} catch (error) { } catch (error) {
console.error("Error fetching OTP data:", error); console.error("❌ Gagal mengambil data OTP:", error);
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Gagal mengambil data OTP" }, { success: false, message: "Terjadi kesalahan internal" },
{ status: 500 } { status: 500 }
); );
} finally { } finally {

View File

@@ -14,7 +14,7 @@ export async function POST(req: Request) {
if (await prisma.user.findUnique({ where: { nomor } })) { if (await prisma.user.findUnique({ where: { nomor } })) {
return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 }); 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 }); return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
} }

View File

@@ -1,68 +1,55 @@
// src/app/api/auth/resend-otp/route.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { randomOTP } from "../_lib/randomOTP";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { randomOTP } from "../_lib/randomOTP";
export async function POST(req: Request) { export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
{ success: false, message: "Method Not Allowed" },
{ status: 405 }
);
}
try { try {
const codeOtp = randomOTP(); const { nomor } = await req.json();
const body = await req.json();
const { nomor } = body;
const res = await fetch( if (!nomor || typeof nomor !== 'string') {
`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")
return NextResponse.json( return NextResponse.json(
{ { success: false, message: "Nomor tidak valid" },
success: false,
message: "Nomor Whatsapp Tidak Aktif",
},
{ status: 400 } { 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: { data: {
nomor: nomor, nomor,
otp: codeOtp, otp: otpNumber,
isActive: true,
}, },
}); });
if (!createOtpId) return NextResponse.json({
return NextResponse.json( success: true,
{ message: "OTP baru dikirim",
success: false, kodeId: otpRecord.id,
message: "Gagal Membuat Kode OTP", });
},
{ status: 400 }
);
return NextResponse.json(
{
success: true,
message: "Kode Verifikasi Dikirim",
kodeId: createOtpId.id,
},
{ status: 200 }
);
} catch (error) { } catch (error) {
console.error(" Error Resend OTP", error); console.error("Error Resend OTP:", error);
return NextResponse.json( return NextResponse.json(
{ { success: false, message: "Gagal mengirim ulang OTP" },
success: false,
message: "Server Whatsapp Error !!",
},
{ status: 500 } { status: 500 }
); );
} finally { } finally {

View File

@@ -14,7 +14,7 @@ export async function POST(req: Request) {
if (await prisma.user.findUnique({ where: { nomor } })) { if (await prisma.user.findUnique({ where: { nomor } })) {
return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 }); 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 }); return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
} }

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -6,55 +6,77 @@ import { Center, Loader, Paper, Stack, Text, Title } from '@mantine/core';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
// Ganti ini jika tidak pakai next-auth
async function fetchUser() { async function fetchUser() {
const res = await fetch('/api/auth/me'); 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(); return res.json();
} }
export default function WaitingRoom() { export default function WaitingRoom() {
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<any>(null); const [user, setUser] = useState<any>(null);
// const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isRedirecting, setIsRedirecting] = useState(false);
useEffect(() => {
let isMounted = true;
const interval = setInterval(async () => {
if (isRedirecting || !isMounted) return;
useEffect(() => { try {
let isMounted = true; const data = await fetchUser();
const interval = setInterval(async () => { if (!isMounted) return;
try {
const data = await fetchUser();
if (!isMounted) return;
const currentUser = data.user; const currentUser = data.user;
setUser(currentUser); setUser(currentUser);
// ✅ Sekarang isActive tersedia! // ✅ Periksa isActive dan redirect
if (currentUser?.isActive) { if (currentUser?.isActive === true) {
clearInterval(interval); setIsRedirecting(true);
// Redirect ke halaman admin sesuai role clearInterval(interval);
if (currentUser.roleId === 0) {
router.push('/admin/landing-page/profil/program-inovasi'); // ✅ 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 { } else {
router.push('/admin'); // atau halaman default role console.error('Error polling:', err);
} }
} }
} catch (err: any) { }, 3000);
if (!isMounted) return;
setError(err.message || 'Gagal memuat status');
clearInterval(interval);
if (err.message === 'Unauthorized') {
router.push('/login');
}
}
}, 2000);
return () => { return () => {
isMounted = false; isMounted = false;
clearInterval(interval); clearInterval(interval);
}; };
}, [router]); }, [router, isRedirecting]);
if (error) { if (error) {
return ( return (
@@ -69,6 +91,24 @@ useEffect(() => {
); );
} }
if (isRedirecting) {
return (
<Center h="100vh" bg={colors.Bg}>
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
<Stack align="center" gap="lg">
<Title order={2} c={colors['blue-button']} ta="center">
Akun Disetujui!
</Title>
<Text ta="center" c="green">
Mengalihkan ke dashboard...
</Text>
<Loader size="sm" color="green" />
</Stack>
</Paper>
</Center>
);
}
return ( return (
<Center h="100vh" bg={colors.Bg}> <Center h="100vh" bg={colors.Bg}>
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}> <Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
@@ -76,17 +116,13 @@ useEffect(() => {
<Title order={2} c={colors['blue-button']} ta="center"> <Title order={2} c={colors['blue-button']} ta="center">
Menunggu Persetujuan Menunggu Persetujuan
</Title> </Title>
<Text ta="center" c="dimmed"> <Text ta="center" c="dimmed">
Akun Anda sedang dalam proses verifikasi oleh Superadmin. Akun Anda sedang dalam proses verifikasi oleh Superadmin.
</Text> </Text>
<Text ta="center" size="sm" c="dimmed"> <Text ta="center" size="sm" c="dimmed">
Nomor: {user?.nomor || '...'} Nomor: {user?.nomor || '...'}
</Text> </Text>
<Loader size="sm" color={colors['blue-button']} /> <Loader size="sm" color={colors['blue-button']} />
<Text ta="center" size="xs" c="dimmed"> <Text ta="center" size="xs" c="dimmed">
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui. Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
</Text> </Text>