Compare commits
4 Commits
nico/21-no
...
nico/25-no
| Author | SHA1 | Date | |
|---|---|---|---|
| e30b27f7a4 | |||
| e941ed3893 | |||
| ace5aff1b6 | |||
| 716db0adca |
@@ -54,6 +54,7 @@
|
|||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -2164,17 +2164,20 @@ enum StatusPeminjaman {
|
|||||||
|
|
||||||
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?
|
|
||||||
|
sessions UserSession[] // ✅ Relasi one-to-many
|
||||||
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
|
menuAccesses UserMenuAccess[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
@@ -2201,13 +2204,30 @@ 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 {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
menuId String // ID menu (misal: "Landing Page", "Kesehatan")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@unique([userId, menuId]) // Satu user tidak bisa punya akses menu yang sama dua kali
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= DATA PENDIDIKAN ========================================= //
|
// ========================================= DATA PENDIDIKAN ========================================= //
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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, 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,37 +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, '') ?? '';
|
||||||
|
if (cleanNomor.length < 10) {
|
||||||
|
toast.error('Nomor tidak valid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username.trim().length < 5) {
|
||||||
|
toast.error('Username minimal 5 karakter');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Verifikasi OTP via endpoint register
|
||||||
|
const verifyRes = await fetch('/api/auth/verify-otp-register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor, username, otp, kodeId }),
|
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const verifyData = await verifyRes.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (!verifyRes.ok) {
|
||||||
// Set user sementara (tanpa roleId, akan diisi saat approve)
|
toast.error(verifyData.message || 'Verifikasi OTP gagal');
|
||||||
authStore.setUser({
|
return;
|
||||||
id: 'pending',
|
|
||||||
name: username,
|
|
||||||
});
|
|
||||||
cleanupStorage();
|
|
||||||
router.replace('/waiting-room');
|
|
||||||
} else {
|
|
||||||
toast.error(data.message || 'Registrasi gagal');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error registrasi:', error);
|
// 2. Finalisasi registrasi
|
||||||
toast.error('Gagal menyelesaikan 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();
|
||||||
|
window.location.href = '/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) {
|
||||||
|
window.location.href = '/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';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,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 }),
|
||||||
@@ -189,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">
|
||||||
@@ -208,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>
|
||||||
@@ -219,6 +245,7 @@ export default function Validasi() {
|
|||||||
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
||||||
Masukkan Kode Verifikasi
|
Masukkan Kode Verifikasi
|
||||||
</Text>
|
</Text>
|
||||||
|
<Center>
|
||||||
<PinInput
|
<PinInput
|
||||||
length={4}
|
length={4}
|
||||||
value={otp}
|
value={otp}
|
||||||
@@ -227,6 +254,7 @@ export default function Validasi() {
|
|||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
34
src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
Normal file
34
src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
|
||||||
|
import { devBar, navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin';
|
||||||
|
|
||||||
|
// ✅ Helper: normalisasi ID menu agar konsisten
|
||||||
|
const normalizeMenuId = (id: string): string => {
|
||||||
|
return id.trim().toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getNavbar({
|
||||||
|
roleId,
|
||||||
|
menuIds,
|
||||||
|
}: {
|
||||||
|
roleId: number;
|
||||||
|
menuIds?: string[] | null;
|
||||||
|
}) {
|
||||||
|
// ✅ Jika menuIds tersedia, gunakan untuk filter — dengan normalisasi
|
||||||
|
if (menuIds && menuIds.length > 0) {
|
||||||
|
// Normalisasi semua menuIds dari DB/state
|
||||||
|
const normalizedMenuSet = new Set(menuIds.map(id => normalizeMenuId(id)));
|
||||||
|
|
||||||
|
return navBar.filter(section => {
|
||||||
|
const normalizedSectionId = normalizeMenuId(section.id);
|
||||||
|
return normalizedMenuSet.has(normalizedSectionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔁 Fallback ke role-based navigation
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
import { IconForms, IconUser } from '@tabler/icons-react';
|
import { IconBrush, IconForms, IconUser } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -23,6 +23,12 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
href: "/admin/user&role/role",
|
href: "/admin/user&role/role",
|
||||||
icon: <IconForms size={18} stroke={1.8} />,
|
icon: <IconForms size={18} stroke={1.8} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Menu Access",
|
||||||
|
value: "menu-access",
|
||||||
|
href: "/admin/user&role/menu-access",
|
||||||
|
icon: <IconBrush size={18} stroke={1.8} />,
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||||
|
|||||||
129
src/app/admin/(dashboard)/user&role/menu-access/page.tsx
Normal file
129
src/app/admin/(dashboard)/user&role/menu-access/page.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
// src/app/admin/user&role/menu-access/page.tsx
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { navBar } from '@/app/admin/_com/list_PageAdmin'
|
||||||
|
import { Button, Checkbox, Group, Paper, Select, Stack, Text, Title } from '@mantine/core'
|
||||||
|
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
|
||||||
|
const extractMenuIds = (navSections: typeof navBar) => {
|
||||||
|
return navSections.map(section => ({
|
||||||
|
value: section.id, // "Landing Page", "Kesehatan", dll
|
||||||
|
label: section.name // "Landing Page", "Kesehatan", dll
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
function MenuAccessPage() {
|
||||||
|
const stateUser = useProxy(user.userState)
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
|
||||||
|
const [userMenus, setUserMenus] = useState<string[]>([])
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
stateUser.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// ✅ Gunakan helper untuk ekstrak menu
|
||||||
|
const availableMenus = extractMenuIds(navBar);
|
||||||
|
|
||||||
|
// Ambil data menu akses user
|
||||||
|
const loadUserMenuAccess = async () => {
|
||||||
|
if (!selectedUserId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ✅ Perbaiki URL: gunakan query string bukan dynamic route
|
||||||
|
const res = await fetch(`/api/admin/user-menu-access?userId=${selectedUserId}`)
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setUserMenus(data.menuIds || [])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal memuat menu akses:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedUserId) {
|
||||||
|
loadUserMenuAccess()
|
||||||
|
}
|
||||||
|
}, [selectedUserId])
|
||||||
|
|
||||||
|
const handleToggleMenu = (menuId: string) => {
|
||||||
|
setUserMenus(prev =>
|
||||||
|
prev.includes(menuId)
|
||||||
|
? prev.filter(id => id !== menuId)
|
||||||
|
: [...prev, menuId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!selectedUserId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/user-menu-access', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userId: selectedUserId, menuIds: userMenus }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.success) {
|
||||||
|
alert('Menu akses berhasil disimpan')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal menyimpan menu akses:', error)
|
||||||
|
alert('Terjadi kesalahan')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={2}>Tampilan Menu</Title>
|
||||||
|
<Paper p="xl" shadow="md" radius="md">
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group>
|
||||||
|
<Text fw={500}>Pilih User:</Text>
|
||||||
|
<Select
|
||||||
|
placeholder="Pilih user"
|
||||||
|
data={stateUser.findMany.data.map(u => ({
|
||||||
|
value: u.id,
|
||||||
|
label: `${u.username} (${u.nomor})`,
|
||||||
|
}))}
|
||||||
|
value={selectedUserId}
|
||||||
|
onChange={setSelectedUserId}
|
||||||
|
w={300}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{selectedUserId && (
|
||||||
|
<>
|
||||||
|
<Text fw={500}>Menu yang Bisa Diakses:</Text>
|
||||||
|
<Stack>
|
||||||
|
{availableMenus.map(menu => (
|
||||||
|
<Checkbox
|
||||||
|
key={menu.value}
|
||||||
|
label={menu.label}
|
||||||
|
checked={userMenus.includes(menu.value)}
|
||||||
|
onChange={() => handleToggleMenu(menu.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button onClick={handleSave} mt="md">
|
||||||
|
Simpan Perubahan
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuAccessPage
|
||||||
@@ -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,7 +96,17 @@ function ListUser({ search }: { search: string }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// Reload data setelah berhasil update
|
// ✅ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,11 +121,24 @@ 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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredData = data || [];
|
const filteredData = (data || []).filter(
|
||||||
|
(item) => item.roleId !== "0" // asumsikan id role SUPERADMIN = "0"
|
||||||
|
);
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
@@ -158,7 +182,9 @@ function ListUser({ search }: { search: string }) {
|
|||||||
<TableTd style={{ width: '20%' }}>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<Select
|
<Select
|
||||||
placeholder="Pilih role"
|
placeholder="Pilih role"
|
||||||
data={stateRole.findMany.data.map((r) => ({
|
data={stateRole.findMany.data
|
||||||
|
.filter(r => r.id !== "0") // ❌ Sembunyikan SUPERADMIN
|
||||||
|
.map(r => ({
|
||||||
label: r.name,
|
label: r.name,
|
||||||
value: r.id,
|
value: r.id,
|
||||||
}))}
|
}))}
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -392,6 +796,11 @@ export const navBar = [
|
|||||||
id: "Role",
|
id: "Role",
|
||||||
name: "Role",
|
name: "Role",
|
||||||
path: "/admin/user&role/role"
|
path: "/admin/user&role/role"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Menu Access",
|
||||||
|
name: "Menu Access",
|
||||||
|
path: "/admin/user&role/menu-access"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,263 +1,3 @@
|
|||||||
// /* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
// 'use client'
|
|
||||||
|
|
||||||
// import colors from "@/con/colors";
|
|
||||||
// import {
|
|
||||||
// ActionIcon,
|
|
||||||
// AppShell,
|
|
||||||
// AppShellHeader,
|
|
||||||
// AppShellMain,
|
|
||||||
// AppShellNavbar,
|
|
||||||
// Burger,
|
|
||||||
// Flex,
|
|
||||||
// Group,
|
|
||||||
// Image,
|
|
||||||
// NavLink,
|
|
||||||
// ScrollArea,
|
|
||||||
// Text,
|
|
||||||
// Tooltip,
|
|
||||||
// rem
|
|
||||||
// } from "@mantine/core";
|
|
||||||
// import { useDisclosure } from "@mantine/hooks";
|
|
||||||
// import {
|
|
||||||
// IconChevronLeft,
|
|
||||||
// IconChevronRight,
|
|
||||||
// IconLogout2
|
|
||||||
// } from "@tabler/icons-react";
|
|
||||||
// import _ from "lodash";
|
|
||||||
// import Link from "next/link";
|
|
||||||
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
|
||||||
// import { navBar } from "./_com/list_PageAdmin";
|
|
||||||
|
|
||||||
// export default function Layout({ children }: { children: React.ReactNode }) {
|
|
||||||
// const [opened, { toggle }] = useDisclosure();
|
|
||||||
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
|
||||||
// const router = useRouter();
|
|
||||||
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <AppShell
|
|
||||||
// suppressHydrationWarning
|
|
||||||
// header={{ height: 64 }}
|
|
||||||
// navbar={{
|
|
||||||
// width: { base: 260, sm: 280, lg: 300 },
|
|
||||||
// breakpoint: 'sm',
|
|
||||||
// collapsed: {
|
|
||||||
// mobile: !opened,
|
|
||||||
// desktop: !desktopOpened,
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// padding="md"
|
|
||||||
// >
|
|
||||||
// <AppShellHeader
|
|
||||||
// style={{
|
|
||||||
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
|
||||||
// borderBottom: `1px solid ${colors["blue-button"]}20`,
|
|
||||||
// padding: '0 16px',
|
|
||||||
// }}
|
|
||||||
// px={{ base: 'sm', sm: 'md' }}
|
|
||||||
// py={{ base: 'xs', sm: 'sm' }}
|
|
||||||
// >
|
|
||||||
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
|
|
||||||
// <Flex align="center" gap="sm">
|
|
||||||
// <Image
|
|
||||||
// src="/assets/images/darmasaba-icon.png"
|
|
||||||
// alt="Logo Darmasaba"
|
|
||||||
// w={{ base: 32, sm: 40 }}
|
|
||||||
// h={{ base: 32, sm: 40 }}
|
|
||||||
// radius="md"
|
|
||||||
// loading="lazy"
|
|
||||||
// style={{
|
|
||||||
// minWidth: '32px',
|
|
||||||
// height: 'auto',
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// <Text
|
|
||||||
// fw={700}
|
|
||||||
// c={colors["blue-button"]}
|
|
||||||
// fz={{ base: 'md', sm: 'xl' }}
|
|
||||||
// >
|
|
||||||
// Admin Darmasaba
|
|
||||||
// </Text>
|
|
||||||
// </Flex>
|
|
||||||
|
|
||||||
// <Group gap="xs">
|
|
||||||
// {!desktopOpened && (
|
|
||||||
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// variant="light"
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// onClick={toggleDesktop}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// >
|
|
||||||
// <IconChevronRight />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// <Burger
|
|
||||||
// opened={opened}
|
|
||||||
// onClick={toggle}
|
|
||||||
// hiddenFrom="sm"
|
|
||||||
// size="md"
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// mr="xs"
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// onClick={() => {
|
|
||||||
// router.push("/darmasaba");
|
|
||||||
// }}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// variant="gradient"
|
|
||||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
|
||||||
// >
|
|
||||||
// <Image
|
|
||||||
// src="/assets/images/darmasaba-icon.png"
|
|
||||||
// alt="Logo Darmasaba"
|
|
||||||
// w={20}
|
|
||||||
// h={20}
|
|
||||||
// radius="md"
|
|
||||||
// loading="lazy"
|
|
||||||
// style={{
|
|
||||||
// minWidth: '20px',
|
|
||||||
// height: 'auto',
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// <Tooltip label="Keluar" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// onClick={() => {
|
|
||||||
// router.push("/darmasaba");
|
|
||||||
// }}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// variant="gradient"
|
|
||||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
|
||||||
// >
|
|
||||||
// <IconLogout2 size={22} />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// </Group>
|
|
||||||
// </Group>
|
|
||||||
// </AppShellHeader>
|
|
||||||
|
|
||||||
// <AppShellNavbar
|
|
||||||
// component={ScrollArea}
|
|
||||||
// style={{
|
|
||||||
// background: "#ffffff",
|
|
||||||
// borderRight: `1px solid ${colors["blue-button"]}20`,
|
|
||||||
// }}
|
|
||||||
// p={{ base: 'xs', sm: 'sm' }}
|
|
||||||
// >
|
|
||||||
// <AppShell.Section p="sm">
|
|
||||||
// {navBar.map((v, k) => {
|
|
||||||
// const isParentActive = segments.includes(_.lowerCase(v.name));
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <NavLink
|
|
||||||
// key={k}
|
|
||||||
// defaultOpened={isParentActive}
|
|
||||||
// c={isParentActive ? colors["blue-button"] : "gray"}
|
|
||||||
// label={
|
|
||||||
// <Text fw={isParentActive ? 600 : 400} fz="sm">
|
|
||||||
// {v.name}
|
|
||||||
// </Text>
|
|
||||||
// }
|
|
||||||
// style={{
|
|
||||||
// borderRadius: rem(10),
|
|
||||||
// marginBottom: rem(4),
|
|
||||||
// transition: "background 150ms ease",
|
|
||||||
// }}
|
|
||||||
// styles={{
|
|
||||||
// root: {
|
|
||||||
// '&:hover': {
|
|
||||||
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// variant="light"
|
|
||||||
// active={isParentActive}
|
|
||||||
// >
|
|
||||||
// {v.children.map((child, key) => {
|
|
||||||
// const isChildActive = segments.includes(
|
|
||||||
// _.lowerCase(child.name)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <NavLink
|
|
||||||
// key={key}
|
|
||||||
// href={child.path}
|
|
||||||
// c={isChildActive ? colors["blue-button"] : "gray"}
|
|
||||||
// label={
|
|
||||||
// <Text fw={isChildActive ? 600 : 400} fz="sm">
|
|
||||||
// {child.name}
|
|
||||||
// </Text>
|
|
||||||
// }
|
|
||||||
// styles={{
|
|
||||||
// root: {
|
|
||||||
// borderRadius: rem(8),
|
|
||||||
// marginBottom: rem(2),
|
|
||||||
// transition: 'background 150ms ease',
|
|
||||||
// padding: '6px 12px',
|
|
||||||
// '&:hover': {
|
|
||||||
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
|
||||||
// },
|
|
||||||
// ...(isChildActive && {
|
|
||||||
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// active={isChildActive}
|
|
||||||
// component={Link}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </NavLink>
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </AppShell.Section>
|
|
||||||
|
|
||||||
// <AppShell.Section py="md">
|
|
||||||
// <Group justify="end" pr="sm">
|
|
||||||
// <Tooltip
|
|
||||||
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
|
||||||
// position="top"
|
|
||||||
// withArrow
|
|
||||||
// >
|
|
||||||
// <ActionIcon
|
|
||||||
// variant="light"
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// onClick={toggleDesktop}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// >
|
|
||||||
// <IconChevronLeft />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// </Group>
|
|
||||||
// </AppShell.Section>
|
|
||||||
// </AppShellNavbar>
|
|
||||||
|
|
||||||
// <AppShellMain
|
|
||||||
// style={{
|
|
||||||
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
|
||||||
// minHeight: "100vh",
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// {children}
|
|
||||||
// </AppShellMain>
|
|
||||||
// </AppShell>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
@@ -290,9 +30,8 @@ import _ from "lodash";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { navigationByRole } from "./_com/navigationByRole";
|
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import { toast } from "react-toastify";
|
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const [opened, { toggle }] = useDisclosure();
|
||||||
@@ -302,14 +41,14 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||||
|
|
||||||
const { user } = useSnapshot(authStore);
|
const { user } = useSnapshot(authStore);
|
||||||
|
|
||||||
console.log("Current user in store:", user);
|
console.log("Current user in store:", user);
|
||||||
|
|
||||||
// ✅ Fetch user dari backend jika belum ada di store
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (authStore.user) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
} // Sudah ada → jangan fetch
|
}
|
||||||
|
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -317,10 +56,19 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
|
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
||||||
|
const menuData = await menuRes.json();
|
||||||
|
|
||||||
|
// ✅ Clone ke array mutable
|
||||||
|
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
||||||
|
? [...menuData.menuIds] // Converts readonly array to mutable
|
||||||
|
: null;
|
||||||
|
|
||||||
authStore.setUser({
|
authStore.setUser({
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
name: data.user.name,
|
name: data.user.name,
|
||||||
roleId: Number(data.user.roleId),
|
roleId: Number(data.user.roleId),
|
||||||
|
menuIds,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
@@ -336,94 +84,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}, [user, router]); // ✅ Sekarang 'user' terdefinisi
|
}, [router]);
|
||||||
|
|
||||||
// ✅ Polling untuk cek perubahan role setiap 30 detik
|
|
||||||
// Di layout.tsx - useEffect polling
|
|
||||||
useEffect(() => {
|
|
||||||
if (!user?.id) return;
|
|
||||||
|
|
||||||
const checkRoleUpdate = async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/auth/me');
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
// ✅ Session tidak valid (sudah dihapus karena role berubah)
|
|
||||||
if (!data.success || !data.user) {
|
|
||||||
console.log('⚠️ Session tidak valid, logout...');
|
|
||||||
authStore.setUser(null);
|
|
||||||
|
|
||||||
// Clear cookie manual (backup)
|
|
||||||
document.cookie = `${process.env.NEXT_PUBLIC_SESSION_KEY}=; Max-Age=0; path=/;`;
|
|
||||||
|
|
||||||
toast.info('Role Anda telah diubah. Silakan login kembali.', {
|
|
||||||
autoClose: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push('/login');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cek perubahan roleId (seharusnya tidak sampai sini jika session dihapus)
|
|
||||||
const currentRoleId = Number(data.user.roleId);
|
|
||||||
|
|
||||||
if (currentRoleId !== user.roleId) {
|
|
||||||
console.log('🔄 Role berubah! Dari', user.roleId, 'ke', currentRoleId);
|
|
||||||
|
|
||||||
// Update store
|
|
||||||
authStore.setUser({
|
|
||||||
id: data.user.id,
|
|
||||||
name: data.user.name || data.user.username,
|
|
||||||
roleId: currentRoleId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Redirect ke halaman default role baru
|
|
||||||
let redirectPath = '/admin';
|
|
||||||
|
|
||||||
switch (currentRoleId) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
redirectPath = '/admin/kesehatan/posyandu';
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.info('Role Anda telah diubah. Mengalihkan halaman...', {
|
|
||||||
autoClose: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push(redirectPath);
|
|
||||||
|
|
||||||
// Reload untuk clear semua state
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error checking role update:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ✅ Polling setiap 10 detik (lebih responsif dari 30 detik)
|
|
||||||
const interval = setInterval(checkRoleUpdate, 10000);
|
|
||||||
|
|
||||||
// Juga cek saat window focus (user kembali ke tab)
|
|
||||||
const handleFocus = () => {
|
|
||||||
checkRoleUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('focus', handleFocus);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
window.removeEventListener('focus', handleFocus);
|
|
||||||
};
|
|
||||||
}, [user, router]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -438,8 +99,8 @@ useEffect(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Ambil menu berdasarkan roleId
|
// ✅ Ambil menu berdasarkan roleId
|
||||||
const currentNav = user?.roleId !== undefined
|
const currentNav = authStore.user
|
||||||
? (navigationByRole[user.roleId as keyof typeof navigationByRole] || [])
|
? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
@@ -667,4 +328,3 @@ useEffect(() => {
|
|||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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) => {
|
||||||
|
// 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 },
|
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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,61 +2,45 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
// API update user
|
||||||
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",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: cek apakah roleId valid
|
// Validasi role
|
||||||
if (roleId) {
|
if (roleId) {
|
||||||
const cekRole = await prisma.role.findUnique({
|
const role = await prisma.role.findUnique({ where: { id: roleId } });
|
||||||
where: { id: roleId }
|
if (!role) return { success: false, message: "Role tidak ditemukan" };
|
||||||
});
|
|
||||||
|
|
||||||
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({
|
const currentUser = await prisma.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
select: {
|
select: { roleId: true, isActive: true }
|
||||||
roleId: true,
|
|
||||||
username: true,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentUser && currentUser.roleId !== roleId) {
|
if (!currentUser) {
|
||||||
isRoleChanged = true;
|
return { success: false, message: "User tidak ditemukan" };
|
||||||
oldRoleId = currentUser.roleId;
|
|
||||||
console.log(`🔄 Role berubah untuk ${currentUser.username}: ${oldRoleId} → ${roleId}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRoleChanged = roleId && currentUser.roleId !== roleId;
|
||||||
|
const isActiveChanged = isActive !== undefined && currentUser.isActive !== isActive;
|
||||||
|
|
||||||
// Update user
|
// 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: invalidate semua sesi
|
||||||
|
...(isRoleChanged ? { sessionInvalid: true } : {}),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -64,48 +48,26 @@ export default async function userUpdate(context: Context) {
|
|||||||
nomor: true,
|
nomor: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
roleId: true,
|
roleId: true,
|
||||||
updatedAt: true,
|
role: { select: { name: true } }
|
||||||
role: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ FORCE LOGOUT: Hapus UserSession jika role berubah
|
// ✅ HAPUS SEMUA SESI USER DI DATABASE
|
||||||
if (isRoleChanged) {
|
if (isRoleChanged) {
|
||||||
try {
|
await prisma.userSession.deleteMany({ where: { userId: id } });
|
||||||
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 {
|
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,
|
||||||
roleChanged: isRoleChanged, // Info untuk frontend
|
message: isRoleChanged
|
||||||
oldRoleId: oldRoleId,
|
? `Role ${updatedUser.username} diubah. User akan logout otomatis.`
|
||||||
newRoleId: roleId,
|
: 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);
|
||||||
return {
|
return {
|
||||||
|
|||||||
65
src/app/api/admin/user-menu-access/route.ts
Normal file
65
src/app/api/admin/user-menu-access/route.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// src/app/api/admin/user-menu-access/route.ts
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
|
||||||
|
// ❌ HAPUS { params } karena tidak dipakai
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const userId = searchParams.get('userId')
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'User ID diperlukan' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuAccess = await prisma.userMenuAccess.findMany({
|
||||||
|
where: { userId },
|
||||||
|
select: { menuId: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
menuIds: menuAccess.map(m => m.menuId),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GET User Menu Access Error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Gagal memuat menu akses' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST tetap sama (tanpa perubahan)
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { userId, menuIds } = await request.json()
|
||||||
|
|
||||||
|
if (!userId || !Array.isArray(menuIds)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Data tidak valid' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.userMenuAccess.deleteMany({ where: { userId } })
|
||||||
|
|
||||||
|
if (menuIds.length > 0) {
|
||||||
|
await prisma.userMenuAccess.createMany({
|
||||||
|
data: menuIds.map((menuId: string) => ({ userId, menuId })),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('POST User Menu Access Error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Gagal menyimpan menu akses' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,35 +86,57 @@ 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,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Di dalam api_fetch_auth.ts
|
||||||
|
|
||||||
|
export async function apiFetchUserMenuAccess(userId: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
menuIds?: string[];
|
||||||
|
message?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/user-menu-access/${userId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Fetch User Menu Access Error:', error);
|
||||||
|
return { success: false, message: 'Gagal memuat menu akses' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiUpdateUserMenuAccess(
|
||||||
|
userId: string,
|
||||||
|
menuIds: string[]
|
||||||
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/user-menu-access', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userId, menuIds }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Update User Menu Access Error:', error);
|
||||||
|
return { success: false, message: 'Gagal menyimpan menu akses' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { encrypt } from "./encrypt";
|
import { encrypt } from "./encrypt";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { add } from "date-fns";
|
||||||
|
|
||||||
export async function sessionCreate({
|
export async function sessionCreate({
|
||||||
sessionKey,
|
sessionKey,
|
||||||
@@ -12,79 +13,49 @@ export async function sessionCreate({
|
|||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
exp?: string;
|
exp?: string;
|
||||||
jwtSecret: string;
|
jwtSecret: string;
|
||||||
user: Record<string, unknown>;
|
user: Record<string, unknown> & { id: string };
|
||||||
}) {
|
}) {
|
||||||
// 🔒 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
|
// ✅ Hitung expiresAt sesuai exp
|
||||||
const userId = user.id as string;
|
let expiresAt = add(new Date(), { days: 30 });
|
||||||
|
if (exp === "7 day") expiresAt = add(new Date(), { days: 7 });
|
||||||
|
// tambahkan opsi lain jika perlu
|
||||||
|
|
||||||
if (userId) {
|
// Sebelum create session baru, nonaktifkan session aktif sebelumnya
|
||||||
try {
|
await prisma.userSession.updateMany({
|
||||||
// Hapus session lama user ini (logout device lain)
|
where: { userId: user.id, active: true },
|
||||||
await prisma.userSession.deleteMany({
|
data: { active: false },
|
||||||
where: { userId },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parse expiration
|
// ✅ Simpan ke database
|
||||||
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({
|
await prisma.userSession.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
token,
|
||||||
token, // JWT token disimpan
|
userId: user.id,
|
||||||
expires: expiresDate,
|
|
||||||
active: true,
|
active: true,
|
||||||
|
expiresAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✅ Session created for user ${userId}`);
|
// ✅ Set cookie
|
||||||
} catch (dbError) {
|
(await cookies()).set(sessionKey, token, {
|
||||||
console.error("⚠️ Error menyimpan session ke database:", dbError);
|
|
||||||
// Tetap lanjut meski gagal simpan ke DB (fallback ke JWT only)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cookie
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
cookieStore.set(sessionKey, token, {
|
|
||||||
httpOnly: true,
|
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, // seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
|
|||||||
@@ -1,90 +1,45 @@
|
|||||||
// 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";
|
||||||
|
|
||||||
/**
|
export async function verifySession() {
|
||||||
* 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 {
|
try {
|
||||||
const sessionKey = process.env.BASE_SESSION_KEY;
|
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;
|
const jwtSecret = process.env.BASE_TOKEN_KEY;
|
||||||
if (!jwtSecret) {
|
if (!sessionKey || !jwtSecret) throw new Error('Env tidak lengkap');
|
||||||
throw new Error('BASE_TOKEN_KEY tidak ditemukan di environment');
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const token = (await cookies()).get(sessionKey)?.value;
|
||||||
const token = cookieStore.get(sessionKey)?.value;
|
if (!token) return null;
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Decrypt JWT
|
|
||||||
const jwtUser = await decrypt({ token, jwtSecret });
|
const jwtUser = await decrypt({ token, jwtSecret });
|
||||||
|
if (!jwtUser?.id) return null;
|
||||||
|
|
||||||
if (!jwtUser || !jwtUser.id) {
|
// Cari session di DB berdasarkan token
|
||||||
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({
|
const dbSession = await prisma.userSession.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: jwtUser.id as string,
|
token,
|
||||||
token: token,
|
|
||||||
active: true,
|
active: true,
|
||||||
OR: [
|
expiresAt: { gte: new Date() }
|
||||||
{ expires: null },
|
|
||||||
{ expires: { gte: new Date() } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
User: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
nomor: true,
|
|
||||||
roleId: true,
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
include: { user: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Token tidak ditemukan di database = sudah dihapus (force logout)
|
|
||||||
if (!dbSession) {
|
if (!dbSession) {
|
||||||
console.log('⚠️ Token valid tapi sudah dihapus dari database (force logout)');
|
console.log('⚠️ Session tidak ditemukan di DB');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Return data user terbaru dari database
|
// ❌ Hanya tolak jika sessionInvalid = true
|
||||||
// Ini penting agar roleId selalu update
|
if (dbSession.user.sessionInvalid) {
|
||||||
return {
|
console.log('⚠️ Session di-invalidate');
|
||||||
id: dbSession.User.id,
|
return null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
username: newUser.username, // ✅ Pastikan sesuai
|
||||||
roleId: newUser.roleId,
|
roleId: newUser.roleId,
|
||||||
isActive: false, // User baru belum aktif
|
isActive: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} 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(
|
||||||
|
|||||||
@@ -1,44 +1,58 @@
|
|||||||
// app/api/auth/me/route.ts
|
// src/app/api/auth/me/route.ts
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { verifySession } from '../_lib/session_verify';
|
import { verifySession } from '../_lib/session_verify';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// ✅ Verify session (hybrid: JWT + Database)
|
const sessionUser = await verifySession();
|
||||||
const user = await verifySession();
|
if (!sessionUser) {
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ success: false, message: "Unauthorized", user: null },
|
||||||
success: false,
|
|
||||||
message: "Session tidak valid",
|
|
||||||
user: null
|
|
||||||
},
|
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data user sudah fresh dari database (via verifySession)
|
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({
|
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),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
} 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: "Internal server error", user: null },
|
||||||
success: false,
|
|
||||||
message: "Terjadi kesalahan",
|
|
||||||
user: null
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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") {
|
try {
|
||||||
|
const { nomor } = await req.json();
|
||||||
|
|
||||||
|
if (!nomor || typeof nomor !== 'string') {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Method Not Allowed" },
|
{ success: false, message: "Nomor tidak valid" },
|
||||||
{ status: 405 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const codeOtp = randomOTP();
|
const codeOtp = randomOTP();
|
||||||
const body = await req.json();
|
const otpNumber = Number(codeOtp);
|
||||||
const { nomor } = body;
|
|
||||||
|
|
||||||
const res = await fetch(
|
// Kirim OTP via WhatsApp
|
||||||
`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.
|
const waMessage = `Kode verifikasi Anda: ${codeOtp}`;
|
||||||
\n
|
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
|
||||||
>> Kode OTP anda: ${codeOtp}.
|
const waRes = await fetch(waUrl);
|
||||||
`
|
const waData = await waRes.json();
|
||||||
);
|
|
||||||
|
|
||||||
const sendWa = await res.json();
|
if (waData.status !== "success") {
|
||||||
if (sendWa.status !== "success")
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ success: false, message: "Gagal mengirim OTP via WhatsApp" },
|
||||||
success: false,
|
|
||||||
message: "Nomor Whatsapp Tidak Aktif",
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const createOtpId = await prisma.kodeOtp.create({
|
// 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: false,
|
|
||||||
message: "Gagal Membuat Kode OTP",
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true,
|
success: true,
|
||||||
message: "Kode Verifikasi Dikirim",
|
message: "OTP baru dikirim",
|
||||||
kodeId: createOtpId.id,
|
kodeId: otpRecord.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 {
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
101
src/app/api/auth/verify-otp-login/route.ts
Normal file
101
src/app/api/auth/verify-otp-login/route.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/app/api/auth/verify-otp-register/route.ts
Normal file
61
src/app/api/auth/verify-otp-register/route.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,23 +6,27 @@ 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(() => {
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
|
if (isRedirecting || !isMounted) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchUser();
|
const data = await fetchUser();
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
@@ -30,31 +34,49 @@ useEffect(() => {
|
|||||||
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) {
|
||||||
|
setIsRedirecting(true);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
// Redirect ke halaman admin sesuai role
|
|
||||||
if (currentUser.roleId === 0) {
|
// ✅ roleId adalah STRING → gunakan string literal
|
||||||
router.push('/admin/landing-page/profil/program-inovasi');
|
let redirectPath = '/admin';
|
||||||
} else {
|
|
||||||
router.push('/admin'); // atau halaman default role
|
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) {
|
} catch (err: any) {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
setError(err.message || 'Gagal memuat status');
|
|
||||||
|
// ❌ Hanya redirect ke /login jika benar-benar tidak ada sesi
|
||||||
|
if (err.message.includes('401')) {
|
||||||
|
setError('Sesi tidak valid');
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
if (err.message === 'Unauthorized') {
|
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
|
} else {
|
||||||
|
console.error('Error polling:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 3000);
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -1,46 +1,98 @@
|
|||||||
// app/middleware.js
|
// // app/middleware.js
|
||||||
import { NextResponse, NextRequest } from 'next/server';
|
// import { NextResponse, NextRequest } from 'next/server';
|
||||||
|
|
||||||
// Daftar route yang diizinkan tanpa login (public routes)
|
// // Daftar route yang diizinkan tanpa login (public routes)
|
||||||
const publicRoutes = [
|
// const publicRoutes = [
|
||||||
'/*', // Home page
|
// '/*', // Home page
|
||||||
'/about', // About page
|
// '/about', // About page
|
||||||
'/public/*', // Wildcard untuk semua route di bawah /public
|
// '/public/*', // Wildcard untuk semua route di bawah /public
|
||||||
'/login', // Halaman login
|
// '/login', // Halaman login
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// // Fungsi untuk memeriksa apakah route saat ini adalah route publik
|
||||||
|
// function isPublicRoute(pathname: string) {
|
||||||
|
// return publicRoutes.some((route) => {
|
||||||
|
// // Jika route mengandung wildcard (*), gunakan regex untuk mencocokkan
|
||||||
|
// if (route.endsWith('*')) {
|
||||||
|
// const baseRoute = route.replace('*', ''); // Hapus wildcard
|
||||||
|
// return pathname.startsWith(baseRoute); // Cocokkan dengan pathname
|
||||||
|
// }
|
||||||
|
// return pathname === route; // Cocokkan exact path
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export function middleware(request: NextRequest) {
|
||||||
|
// const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
|
// // Jika route adalah public, izinkan akses
|
||||||
|
// if (isPublicRoute(pathname)) {
|
||||||
|
// return NextResponse.next();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Jika bukan public route, periksa apakah pengguna sudah login
|
||||||
|
// const isLoggedIn = request.cookies.get('darmasaba-auth-token'); // Contoh: cek cookie auth-token
|
||||||
|
// if (!isLoggedIn) {
|
||||||
|
// // Redirect ke halaman login jika belum login
|
||||||
|
// return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Jika sudah login, izinkan akses
|
||||||
|
// return NextResponse.next();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Konfigurasi untuk menentukan path mana yang akan dijalankan middleware
|
||||||
|
// export const config = {
|
||||||
|
// matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // Jalankan middleware untuk semua route kecuali file statis
|
||||||
|
// };
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// src/app/admin/middleware.ts
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import { jwtVerify } from 'jose';
|
||||||
|
|
||||||
|
// Route publik di dalam /admin (boleh diakses tanpa login penuh)
|
||||||
|
const PUBLIC_ADMIN_ROUTES = [
|
||||||
|
'/admin/login',
|
||||||
|
'/admin/registrasi',
|
||||||
|
'/admin/validasi',
|
||||||
|
'/admin/waiting-room',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Fungsi untuk memeriksa apakah route saat ini adalah route publik
|
export async function middleware(request: NextRequest) {
|
||||||
function isPublicRoute(pathname: string) {
|
const path = request.nextUrl.pathname;
|
||||||
return publicRoutes.some((route) => {
|
|
||||||
// Jika route mengandung wildcard (*), gunakan regex untuk mencocokkan
|
|
||||||
if (route.endsWith('*')) {
|
|
||||||
const baseRoute = route.replace('*', ''); // Hapus wildcard
|
|
||||||
return pathname.startsWith(baseRoute); // Cocokkan dengan pathname
|
|
||||||
}
|
|
||||||
return pathname === route; // Cocokkan exact path
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
// Izinkan akses ke route publik di /admin
|
||||||
const { pathname } = request.nextUrl;
|
if (PUBLIC_ADMIN_ROUTES.some(route => path.startsWith(route))) {
|
||||||
|
|
||||||
// Jika route adalah public, izinkan akses
|
|
||||||
if (isPublicRoute(pathname)) {
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika bukan public route, periksa apakah pengguna sudah login
|
// Ambil token dari cookie
|
||||||
const isLoggedIn = request.cookies.get('darmasaba-auth-token'); // Contoh: cek cookie auth-token
|
const token = request.cookies.get(process.env.BASE_SESSION_KEY!)?.value;
|
||||||
if (!isLoggedIn) {
|
if (!token) {
|
||||||
// Redirect ke halaman login jika belum login
|
|
||||||
return NextResponse.redirect(new URL('/login', request.url));
|
return NextResponse.redirect(new URL('/login', request.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika sudah login, izinkan akses
|
try {
|
||||||
|
// Verifikasi JWT
|
||||||
|
const secret = new TextEncoder().encode(process.env.BASE_TOKEN_KEY!);
|
||||||
|
const { payload } = await jwtVerify(token, secret);
|
||||||
|
const user = (payload as any).user;
|
||||||
|
|
||||||
|
// Cek apakah user aktif
|
||||||
|
if (!user || !user.isActive) {
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ User valid → izinkan akses
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Middleware auth error:', error);
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konfigurasi untuk menentukan path mana yang akan dijalankan middleware
|
// Hanya berlaku untuk /admin/*
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // Jalankan middleware untuk semua route kecuali file statis
|
matcher: ['/admin/:path*'],
|
||||||
};
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
// src/store/authStore.ts
|
||||||
import { proxy } from 'valtio';
|
import { proxy } from 'valtio';
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
roleId?: number; // 0, 1, 2, 3
|
roleId: number;
|
||||||
|
menuIds?: string[] | null; // ✅ Pastikan pakai `string[]`
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authStore = proxy<{
|
export const authStore = proxy<{
|
||||||
|
|||||||
Reference in New Issue
Block a user