Compare commits
2 Commits
nico/20-no
...
nico/24-no
| Author | SHA1 | Date | |
|---|---|---|---|
| 716db0adca | |||
| a291bdfb51 |
@@ -2163,18 +2163,20 @@ enum StatusPeminjaman {
|
||||
// ========================================= USER ========================================= //
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
nomor String @unique
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
roleId String @default("1")
|
||||
instansi String?
|
||||
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
|
||||
isActive Boolean @default(true)
|
||||
lastLogin DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
nomor String @unique
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
roleId String @default("1")
|
||||
instansi String?
|
||||
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
|
||||
isActive Boolean @default(true)
|
||||
lastLogin DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
sessionInvalid Boolean @default(false)
|
||||
UserMenuAccess UserMenuAccess[]
|
||||
}
|
||||
|
||||
model Role {
|
||||
@@ -2210,6 +2212,18 @@ model UserSession {
|
||||
userId String @unique
|
||||
}
|
||||
|
||||
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 ========================================= //
|
||||
model DataPendidikan {
|
||||
id String @id @default(cuid())
|
||||
|
||||
@@ -90,40 +90,88 @@ const userState = proxy({
|
||||
}
|
||||
},
|
||||
},
|
||||
update: {
|
||||
deleteUser: {
|
||||
loading: false,
|
||||
|
||||
async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
|
||||
this.loading = true;
|
||||
|
||||
async delete(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/updt`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
userState.deleteUser.loading = true;
|
||||
|
||||
const response = await fetch(`/api/user/delUser/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.status === 200 && data.success) {
|
||||
toast.success(data.message);
|
||||
|
||||
// refresh list
|
||||
userState.findMany.load(
|
||||
userState.findMany.page,
|
||||
10,
|
||||
userState.findMany.search
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "User berhasil dihapus permanen");
|
||||
await userState.findMany.load(); // refresh list user setelah delete
|
||||
} else {
|
||||
toast.error(data.message || "Gagal update user");
|
||||
toast.error(result?.message || "Gagal menghapus user");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Gagal update user");
|
||||
} catch (error) {
|
||||
console.error("Gagal delete user:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus user");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
userState.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
// Di file userState.ts atau dimana state user berada
|
||||
|
||||
update: {
|
||||
loading: false,
|
||||
|
||||
async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`/api/user/updt`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.status === 200 && data.success) {
|
||||
// ✅ Tampilkan pesan yang berbeda jika role berubah
|
||||
if (data.roleChanged) {
|
||||
toast.success(
|
||||
`${data.message}\n\nUser akan logout otomatis dalam beberapa detik.`,
|
||||
{
|
||||
autoClose: 5000,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
toast.success(data.message);
|
||||
}
|
||||
|
||||
// Refresh list
|
||||
await userState.findMany.load(
|
||||
userState.findMany.page,
|
||||
10,
|
||||
userState.findMany.search
|
||||
);
|
||||
|
||||
return true; // ✅ Return success untuk handling di component
|
||||
} else {
|
||||
toast.error(data.message || "Gagal update user");
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("❌ Error update user:", e);
|
||||
toast.error("Gagal update user");
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const templateRole = z.object({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// app/validasi/page.tsx
|
||||
//
|
||||
|
||||
'use client';
|
||||
|
||||
import { apiFetchOtpData, apiFetchVerifyOtp } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||
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 { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -17,89 +18,152 @@ export default function Validasi() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [kodeId, setKodeId] = useState<string | null>(null);
|
||||
|
||||
// Inisialisasi data OTP
|
||||
useEffect(() => {
|
||||
const storedKodeId = localStorage.getItem('auth_kodeId');
|
||||
if (!storedKodeId) {
|
||||
toast.error('Akses tidak valid');
|
||||
router.push('/login');
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setKodeId(storedKodeId);
|
||||
|
||||
const fetchOtpData = async () => {
|
||||
const loadOtpData = async () => {
|
||||
try {
|
||||
const result = await apiFetchOtpData({ kodeId: storedKodeId });
|
||||
if (result.success && result.data?.nomor) {
|
||||
setNomor(result.data.nomor);
|
||||
} else {
|
||||
throw new Error('OTP tidak valid');
|
||||
throw new Error('Data OTP tidak valid');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Gagal muat OTP:', error);
|
||||
console.error('Gagal memuat data OTP:', error);
|
||||
toast.error('Kode verifikasi tidak valid');
|
||||
router.push('/login');
|
||||
router.replace('/login');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchOtpData();
|
||||
loadOtpData();
|
||||
}, [router]);
|
||||
|
||||
// Verifikasi OTP
|
||||
const handleVerify = async () => {
|
||||
if (!kodeId || !nomor || otp.length < 4) return;
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId });
|
||||
|
||||
if (verifyResult.success && verifyResult.user) {
|
||||
// ✅ SET USER KE STORE
|
||||
authStore.setUser({
|
||||
id: verifyResult.user.id,
|
||||
name: verifyResult.user.name,
|
||||
roleId: Number(verifyResult.user.roleId),
|
||||
});
|
||||
|
||||
cleanupStorage();
|
||||
router.push('/admin/landing-page/profil/program-inovasi');
|
||||
return;
|
||||
}
|
||||
|
||||
// Hanya coba registrasi jika akun tidak ditemukan
|
||||
if (verifyResult.status === 404 && verifyResult.message?.includes('Akun tidak ditemukan')) {
|
||||
const username = localStorage.getItem('auth_username');
|
||||
if (!username) {
|
||||
toast.error('Data registrasi hilang');
|
||||
|
||||
if (!verifyResult.success) {
|
||||
// Registrasi baru?
|
||||
if (
|
||||
verifyResult.status === 404 &&
|
||||
verifyResult.message?.includes('Akun tidak ditemukan')
|
||||
) {
|
||||
await handleNewRegistration();
|
||||
return;
|
||||
}
|
||||
|
||||
const regRes = await fetch('/api/auth/finalize-registration', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nomor, username, otp, kodeId }),
|
||||
});
|
||||
|
||||
const regData = await regRes.json();
|
||||
if (regData.success) {
|
||||
cleanupStorage();
|
||||
router.push('/waiting-room'); // ✅
|
||||
} else {
|
||||
toast.error(regData.message || 'Registrasi gagal');
|
||||
}
|
||||
} else {
|
||||
// Hanya tampilkan error jika bukan kasus "akun tidak ditemukan"
|
||||
|
||||
// Error lain
|
||||
toast.error(verifyResult.message || 'Verifikasi gagal');
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Verifikasi sukses → simpan user ke store
|
||||
const user = verifyResult.user;
|
||||
console.log('=== DEBUG USER ===');
|
||||
console.log('Full user object:', user);
|
||||
|
||||
if (!user || !user.id) {
|
||||
toast.error('Data pengguna tidak lengkap');
|
||||
return;
|
||||
}
|
||||
|
||||
const roleId = Number(user.roleId);
|
||||
authStore.setUser({
|
||||
id: user.id,
|
||||
name: user.name || user.username || 'User',
|
||||
roleId: roleId,
|
||||
});
|
||||
|
||||
cleanupStorage();
|
||||
|
||||
const isUserActive = user.isActive ?? user.is_active ?? true;
|
||||
|
||||
// Redirect berdasarkan status approval
|
||||
if (!isUserActive) {
|
||||
router.replace('/waiting-room');
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Switch statement lebih clean
|
||||
let redirectPath: string;
|
||||
|
||||
switch (roleId) {
|
||||
case 0:
|
||||
case 1:
|
||||
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
||||
break;
|
||||
case 2:
|
||||
redirectPath = '/admin/kesehatan/posyandu';
|
||||
break;
|
||||
case 3:
|
||||
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
break;
|
||||
default:
|
||||
redirectPath = '/admin';
|
||||
console.warn('Unknown roleId:', roleId);
|
||||
}
|
||||
|
||||
console.log('Redirecting to:', redirectPath);
|
||||
router.replace(redirectPath);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Verifikasi error:', error);
|
||||
toast.error('Terjadi kesalahan');
|
||||
console.error('Error saat verifikasi:', error);
|
||||
toast.error('Terjadi kesalahan sistem');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Registrasi baru
|
||||
const handleNewRegistration = async () => {
|
||||
const username = localStorage.getItem('auth_username');
|
||||
if (!username) {
|
||||
toast.error('Data registrasi tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/finalize-registration', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nomor, username, otp, kodeId }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// Set user sementara (tanpa roleId, akan diisi saat approve)
|
||||
authStore.setUser({
|
||||
id: 'pending',
|
||||
name: username,
|
||||
roleId: 1,
|
||||
});
|
||||
cleanupStorage();
|
||||
router.replace('/waiting-room');
|
||||
} else {
|
||||
toast.error(data.message || 'Registrasi gagal');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error registrasi:', error);
|
||||
toast.error('Gagal menyelesaikan registrasi');
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupStorage = () => {
|
||||
localStorage.removeItem('auth_kodeId');
|
||||
localStorage.removeItem('auth_nomor');
|
||||
@@ -118,12 +182,15 @@ export default function Validasi() {
|
||||
if (data.success) {
|
||||
localStorage.setItem('auth_kodeId', data.kodeId);
|
||||
toast.success('OTP baru dikirim');
|
||||
} else {
|
||||
toast.error(data.message || 'Gagal mengirim ulang OTP');
|
||||
}
|
||||
} catch {
|
||||
toast.error('Gagal kirim ulang');
|
||||
toast.error('Gagal menghubungi server');
|
||||
}
|
||||
};
|
||||
|
||||
// Loading
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
|
||||
@@ -148,19 +215,21 @@ export default function Validasi() {
|
||||
Kami telah mengirim kode ke nomor <strong>{nomor}</strong>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box w="100%">
|
||||
<Box mb={20}>
|
||||
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
||||
Masukkan Kode Verifikasi
|
||||
</Text>
|
||||
<PinInput
|
||||
length={4}
|
||||
value={otp}
|
||||
onChange={setOtp}
|
||||
onComplete={handleVerify}
|
||||
inputMode="numeric"
|
||||
size="lg"
|
||||
/>
|
||||
<Center>
|
||||
<PinInput
|
||||
length={4}
|
||||
value={otp}
|
||||
onChange={setOtp}
|
||||
onComplete={handleVerify}
|
||||
inputMode="numeric"
|
||||
size="lg"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
@@ -176,7 +245,14 @@ export default function Validasi() {
|
||||
|
||||
<Text ta="center" size="sm" mt="md">
|
||||
Tidak menerima kode?{' '}
|
||||
<Button variant="subtle" onClick={handleResend} size="xs" p={0} h="auto" color={colors['blue-button']}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={handleResend}
|
||||
size="xs"
|
||||
p={0}
|
||||
h="auto"
|
||||
color={colors['blue-button']}
|
||||
>
|
||||
Kirim Ulang
|
||||
</Button>
|
||||
</Text>
|
||||
|
||||
22
src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
Normal file
22
src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
|
||||
import { navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin';
|
||||
|
||||
export function getNavbar({
|
||||
roleId,
|
||||
menuIds,
|
||||
}: {
|
||||
roleId: number; // pastikan number
|
||||
menuIds?: string[] | null; // opsional
|
||||
}) {
|
||||
// Prioritas: menuIds > roleId
|
||||
if (menuIds && menuIds.length > 0) {
|
||||
return navBar.filter(section => menuIds.includes(section.id));
|
||||
}
|
||||
|
||||
// Fallback ke role-based
|
||||
if (roleId === 0) return navBar;
|
||||
if (roleId === 1) return role1;
|
||||
if (roleId === 2) return role2;
|
||||
if (roleId === 3) return role3;
|
||||
return [];
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
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 React, { useEffect, useState } from 'react';
|
||||
|
||||
@@ -23,6 +23,12 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
href: "/admin/user&role/role",
|
||||
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);
|
||||
|
||||
124
src/app/admin/(dashboard)/user&role/menu-access/page.tsx
Normal file
124
src/app/admin/(dashboard)/user&role/menu-access/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/* 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'
|
||||
|
||||
|
||||
// ✅ 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[]>([])
|
||||
|
||||
// ✅ 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
|
||||
@@ -2,14 +2,13 @@
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Select, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconCheck, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { IconCheck, IconSearch, IconTrash, IconX } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||
import user from '../../_state/user/user-state';
|
||||
|
||||
|
||||
function User() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
@@ -27,10 +26,10 @@ function User() {
|
||||
}
|
||||
|
||||
function ListUser({ search }: { search: string }) {
|
||||
const stateUser = useProxy(user.userState)
|
||||
const stateRole = useProxy(user.roleState)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const stateUser = useProxy(user.userState);
|
||||
const stateRole = useProxy(user.roleState);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -42,21 +41,99 @@ function ListUser({ search }: { search: string }) {
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
stateUser.delete.submit(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
|
||||
stateUser.findMany.load()
|
||||
stateUser.deleteUser.delete(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
stateUser.findMany.load();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateRole.findMany.load()
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
stateRole.findMany.load();
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
const filteredData = data || []
|
||||
// ✅ Helper function untuk nama role
|
||||
const getRoleName = (roleId: string) => {
|
||||
// Cari dari data role yang sudah diload
|
||||
const role = stateRole.findMany.data.find((r) => r.id === roleId);
|
||||
return role?.name || "Unknown Role";
|
||||
};
|
||||
|
||||
// ✅ Handler untuk perubahan role dengan konfirmasi
|
||||
const handleRoleChange = async (
|
||||
userId: string,
|
||||
username: string,
|
||||
oldRoleId: string,
|
||||
newRoleId: string
|
||||
) => {
|
||||
// Skip jika sama
|
||||
if (oldRoleId === newRoleId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ✅ Konfirmasi perubahan role
|
||||
const confirmed = window.confirm(
|
||||
`⚠️ PERINGATAN\n\n` +
|
||||
`Mengubah role untuk "${username}" akan:\n` +
|
||||
`• Logout user otomatis dari semua device\n` +
|
||||
`• Mengubah akses menu sesuai role baru\n\n` +
|
||||
`Role: ${getRoleName(oldRoleId)} → ${getRoleName(newRoleId)}\n\n` +
|
||||
`Lanjutkan?`
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
// Reload data untuk reset dropdown ke nilai lama
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ Submit update
|
||||
const success = await stateUser.update.submit({
|
||||
id: userId,
|
||||
roleId: newRoleId,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
// Cek apakah role berubah
|
||||
const res = await fetch('/api/user/updt', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: userId,
|
||||
roleId: newRoleId,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.roleChanged) {
|
||||
// Tampilkan notifikasi
|
||||
alert(`User ${username} akan logout otomatis!`);
|
||||
}
|
||||
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
}
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
// ✅ Handler untuk toggle isActive
|
||||
const handleToggleActive = async (userId: string, currentStatus: boolean) => {
|
||||
const success = await stateUser.update.submit({
|
||||
id: userId,
|
||||
isActive: !currentStatus,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = (data || []).filter(
|
||||
(item) => item.roleId !== "0" // asumsikan id role SUPERADMIN = "0"
|
||||
);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
@@ -80,16 +157,19 @@ function ListUser({ search }: { search: string }) {
|
||||
<TableTh style={{ width: '20%' }}>Nomor</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Role</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aktif / Nonaktif</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '25%', }}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>{item.username}</Text>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.username}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%', }}>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Text truncate fz="sm" c="dimmed">
|
||||
{item.nomor}
|
||||
</Text>
|
||||
@@ -97,25 +177,28 @@ function ListUser({ search }: { search: string }) {
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Select
|
||||
placeholder="Pilih role"
|
||||
data={stateRole.findMany.data.map((r) => ({
|
||||
label: r.name,
|
||||
value: r.id,
|
||||
}))}
|
||||
value={item.roleId} // ⬅ role milik user ini
|
||||
onChange={async (val) => {
|
||||
data={stateRole.findMany.data
|
||||
.filter(r => r.id !== "0") // ❌ Sembunyikan SUPERADMIN
|
||||
.map(r => ({
|
||||
label: r.name,
|
||||
value: r.id,
|
||||
}))}
|
||||
value={item.roleId}
|
||||
onChange={(val) => {
|
||||
if (!val) return;
|
||||
|
||||
await stateUser.update.submit({
|
||||
id: item.id,
|
||||
roleId: val, // ⬅ kirim roleId
|
||||
});
|
||||
|
||||
// reload data supaya UI up-to-date
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
// ✅ Panggil handleRoleChange dengan konfirmasi
|
||||
handleRoleChange(
|
||||
item.id,
|
||||
item.username,
|
||||
item.roleId,
|
||||
val
|
||||
);
|
||||
}}
|
||||
searchable
|
||||
clearable={false} // role harus ada
|
||||
clearable={false}
|
||||
nothingFoundMessage="Role tidak ditemukan"
|
||||
disabled={stateUser.update.loading}
|
||||
/>
|
||||
</TableTd>
|
||||
|
||||
@@ -127,26 +210,34 @@ function ListUser({ search }: { search: string }) {
|
||||
<Button
|
||||
variant="light"
|
||||
color={item.isActive ? "green" : "red"}
|
||||
onClick={async () => {
|
||||
await stateUser.update.submit({
|
||||
id: item.id,
|
||||
isActive: !item.isActive, // toggle
|
||||
});
|
||||
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
}}
|
||||
onClick={() => handleToggleActive(item.id, item.isActive)}
|
||||
disabled={stateUser.update.loading}
|
||||
>
|
||||
{item.isActive ? <IconCheck size={20} /> : <IconX size={20} />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
|
||||
<TableTd style={{ width: '15%' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color='red'
|
||||
disabled={stateUser.deleteUser.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<TableTd colSpan={5}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data user yang cocok</Text>
|
||||
<Text c="dimmed">Tidak ada data user yang cocok</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -155,6 +246,7 @@ function ListUser({ search }: { search: string }) {
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -169,6 +261,7 @@ function ListUser({ search }: { search: string }) {
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
@@ -180,4 +273,4 @@ function ListUser({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default User;
|
||||
export default User;
|
||||
@@ -392,6 +392,11 @@ export const navBar = [
|
||||
id: "Role",
|
||||
name: "Role",
|
||||
path: "/admin/user&role/role"
|
||||
},
|
||||
{
|
||||
id: "Menu Access",
|
||||
name: "Menu Access",
|
||||
path: "/admin/user&role/menu-access"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -785,4 +790,4 @@ export const role3 = [
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -1,265 +1,3 @@
|
||||
// 'use client'
|
||||
|
||||
// import colors from "@/con/colors";
|
||||
// import {
|
||||
// ActionIcon,
|
||||
// AppShell,
|
||||
// AppShellHeader,
|
||||
// AppShellMain,
|
||||
// AppShellNavbar,
|
||||
// Burger,
|
||||
// Flex,
|
||||
// Group,
|
||||
// Image,
|
||||
// NavLink,
|
||||
// ScrollArea,
|
||||
// Text,
|
||||
// Tooltip,
|
||||
// rem
|
||||
// } from "@mantine/core";
|
||||
// import { useDisclosure } from "@mantine/hooks";
|
||||
// import {
|
||||
// IconChevronLeft,
|
||||
// IconChevronRight,
|
||||
// IconLogout2
|
||||
// } from "@tabler/icons-react";
|
||||
// import _ from "lodash";
|
||||
// import Link from "next/link";
|
||||
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||
// import { navBar } from "./_com/list_PageAdmin";
|
||||
|
||||
// export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
// const [opened, { toggle }] = useDisclosure();
|
||||
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
// const router = useRouter();
|
||||
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||
|
||||
// return (
|
||||
// <AppShell
|
||||
// suppressHydrationWarning
|
||||
// header={{ height: 64 }}
|
||||
// navbar={{
|
||||
// width: { base: 260, sm: 280, lg: 300 },
|
||||
// breakpoint: 'sm',
|
||||
// collapsed: {
|
||||
// mobile: !opened,
|
||||
// desktop: !desktopOpened,
|
||||
// },
|
||||
// }}
|
||||
// padding="md"
|
||||
// >
|
||||
// <AppShellHeader
|
||||
// style={{
|
||||
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||
// borderBottom: `1px solid ${colors["blue-button"]}20`,
|
||||
// padding: '0 16px',
|
||||
// }}
|
||||
// px={{ base: 'sm', sm: 'md' }}
|
||||
// py={{ base: 'xs', sm: 'sm' }}
|
||||
// >
|
||||
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
|
||||
// <Flex align="center" gap="sm">
|
||||
// <Image
|
||||
// src="/assets/images/darmasaba-icon.png"
|
||||
// alt="Logo Darmasaba"
|
||||
// w={{ base: 32, sm: 40 }}
|
||||
// h={{ base: 32, sm: 40 }}
|
||||
// radius="md"
|
||||
// loading="lazy"
|
||||
// style={{
|
||||
// minWidth: '32px',
|
||||
// height: 'auto',
|
||||
// }}
|
||||
// />
|
||||
// <Text
|
||||
// fw={700}
|
||||
// c={colors["blue-button"]}
|
||||
// fz={{ base: 'md', sm: 'xl' }}
|
||||
// >
|
||||
// Admin Darmasaba
|
||||
// </Text>
|
||||
// </Flex>
|
||||
|
||||
// <Group gap="xs">
|
||||
// {!desktopOpened && (
|
||||
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// variant="light"
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// onClick={toggleDesktop}
|
||||
// color={colors["blue-button"]}
|
||||
// >
|
||||
// <IconChevronRight />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// )}
|
||||
|
||||
// <Burger
|
||||
// opened={opened}
|
||||
// onClick={toggle}
|
||||
// hiddenFrom="sm"
|
||||
// size="md"
|
||||
// color={colors["blue-button"]}
|
||||
// mr="xs"
|
||||
// />
|
||||
|
||||
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// onClick={() => {
|
||||
// router.push("/darmasaba");
|
||||
// }}
|
||||
// color={colors["blue-button"]}
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// variant="gradient"
|
||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
// >
|
||||
// <Image
|
||||
// src="/assets/images/darmasaba-icon.png"
|
||||
// alt="Logo Darmasaba"
|
||||
// w={20}
|
||||
// h={20}
|
||||
// radius="md"
|
||||
// loading="lazy"
|
||||
// style={{
|
||||
// minWidth: '20px',
|
||||
// height: 'auto',
|
||||
// }}
|
||||
// />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// <Tooltip label="Keluar" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// onClick={() => {
|
||||
// router.push("/darmasaba");
|
||||
// }}
|
||||
// color={colors["blue-button"]}
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// variant="gradient"
|
||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
// >
|
||||
// <IconLogout2 size={22} />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// </Group>
|
||||
// </Group>
|
||||
// </AppShellHeader>
|
||||
|
||||
// <AppShellNavbar
|
||||
// component={ScrollArea}
|
||||
// style={{
|
||||
// background: "#ffffff",
|
||||
// borderRight: `1px solid ${colors["blue-button"]}20`,
|
||||
// }}
|
||||
// p={{ base: 'xs', sm: 'sm' }}
|
||||
// >
|
||||
// <AppShell.Section p="sm">
|
||||
// {navBar.map((v, k) => {
|
||||
// const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||
|
||||
// return (
|
||||
// <NavLink
|
||||
// key={k}
|
||||
// defaultOpened={isParentActive}
|
||||
// c={isParentActive ? colors["blue-button"] : "gray"}
|
||||
// label={
|
||||
// <Text fw={isParentActive ? 600 : 400} fz="sm">
|
||||
// {v.name}
|
||||
// </Text>
|
||||
// }
|
||||
// style={{
|
||||
// borderRadius: rem(10),
|
||||
// marginBottom: rem(4),
|
||||
// transition: "background 150ms ease",
|
||||
// }}
|
||||
// styles={{
|
||||
// root: {
|
||||
// '&:hover': {
|
||||
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
||||
// },
|
||||
// },
|
||||
// }}
|
||||
// variant="light"
|
||||
// active={isParentActive}
|
||||
// >
|
||||
// {v.children.map((child, key) => {
|
||||
// const isChildActive = segments.includes(
|
||||
// _.lowerCase(child.name)
|
||||
// );
|
||||
|
||||
// return (
|
||||
// <NavLink
|
||||
// key={key}
|
||||
// href={child.path}
|
||||
// c={isChildActive ? colors["blue-button"] : "gray"}
|
||||
// label={
|
||||
// <Text fw={isChildActive ? 600 : 400} fz="sm">
|
||||
// {child.name}
|
||||
// </Text>
|
||||
// }
|
||||
// styles={{
|
||||
// root: {
|
||||
// borderRadius: rem(8),
|
||||
// marginBottom: rem(2),
|
||||
// transition: 'background 150ms ease',
|
||||
// padding: '6px 12px',
|
||||
// '&:hover': {
|
||||
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
||||
// },
|
||||
// ...(isChildActive && {
|
||||
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||
// }),
|
||||
// },
|
||||
// }}
|
||||
// active={isChildActive}
|
||||
// component={Link}
|
||||
// />
|
||||
// );
|
||||
// })}
|
||||
// </NavLink>
|
||||
// );
|
||||
// })}
|
||||
// </AppShell.Section>
|
||||
|
||||
// <AppShell.Section py="md">
|
||||
// <Group justify="end" pr="sm">
|
||||
// <Tooltip
|
||||
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
||||
// position="top"
|
||||
// withArrow
|
||||
// >
|
||||
// <ActionIcon
|
||||
// variant="light"
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// onClick={toggleDesktop}
|
||||
// color={colors["blue-button"]}
|
||||
// >
|
||||
// <IconChevronLeft />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// </Group>
|
||||
// </AppShell.Section>
|
||||
// </AppShellNavbar>
|
||||
|
||||
// <AppShellMain
|
||||
// style={{
|
||||
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
||||
// minHeight: "100vh",
|
||||
// }}
|
||||
// >
|
||||
// {children}
|
||||
// </AppShellMain>
|
||||
// </AppShell>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'use client'
|
||||
|
||||
import colors from "@/con/colors";
|
||||
@@ -271,9 +9,11 @@ import {
|
||||
AppShellMain,
|
||||
AppShellNavbar,
|
||||
Burger,
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
NavLink,
|
||||
ScrollArea,
|
||||
Text,
|
||||
@@ -289,23 +29,85 @@ import {
|
||||
import _ from "lodash";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { navigationByRole } from "./_com/navigationByRole";
|
||||
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
const router = useRouter();
|
||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||
|
||||
//ambil user dari authStore
|
||||
const {user} = useSnapshot(authStore)
|
||||
console.log("Current User:", user); // 👈 Tambahkan ini
|
||||
const { user } = useSnapshot(authStore);
|
||||
|
||||
//ambil navigation berdasarkan role
|
||||
const currentNav = user?.roleId !== undefined
|
||||
? navigationByRole[user.roleId as keyof typeof navigationByRole] || []
|
||||
: [];
|
||||
console.log("Current user in store:", user);
|
||||
|
||||
useEffect(() => {
|
||||
if (authStore.user) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me');
|
||||
const data = await res.json();
|
||||
|
||||
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({
|
||||
id: data.user.id,
|
||||
name: data.user.name,
|
||||
roleId: Number(data.user.roleId),
|
||||
menuIds,
|
||||
});
|
||||
} else {
|
||||
authStore.setUser(null);
|
||||
router.replace('/login');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data pengguna:', error);
|
||||
authStore.setUser(null);
|
||||
router.replace('/login');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, [router]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<AppShell>
|
||||
<AppShellMain>
|
||||
<Center h="100vh">
|
||||
<Loader />
|
||||
</Center>
|
||||
</AppShellMain>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Ambil menu berdasarkan roleId
|
||||
const currentNav = authStore.user
|
||||
? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
||||
: [];
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.setUser(null);
|
||||
document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`;
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
@@ -388,13 +190,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
variant="gradient"
|
||||
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
>
|
||||
<Image
|
||||
src="/assets/images/darmasaba-icon.png"
|
||||
alt="Logo Darmasaba"
|
||||
w={20}
|
||||
h={20}
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
<Image
|
||||
src="/assets/images/darmasaba-icon.png"
|
||||
alt="Logo Darmasaba"
|
||||
w={20}
|
||||
h={20}
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
style={{
|
||||
minWidth: '20px',
|
||||
height: 'auto',
|
||||
@@ -404,9 +206,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</Tooltip>
|
||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
router.push("/darmasaba");
|
||||
}}
|
||||
onClick={handleLogout}
|
||||
color={colors["blue-button"]}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
@@ -528,4 +328,3 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
38
src/app/api/[[...slugs]]/_lib/user/delUser.ts
Normal file
38
src/app/api/[[...slugs]]/_lib/user/delUser.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// /api/user/delete.ts
|
||||
import prisma from '@/lib/prisma';
|
||||
import { Context } from 'elysia';
|
||||
|
||||
export default async function userDelete(context: Context) {
|
||||
const { id } = context.params as { id: string };
|
||||
|
||||
try {
|
||||
// Cek user dulu
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'User tidak ditemukan',
|
||||
};
|
||||
}
|
||||
|
||||
// Hard delete (hapus permanen)
|
||||
const deletedUser = await prisma.user.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'User berhasil dihapus permanen',
|
||||
data: deletedUser,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error delete user:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Terjadi kesalahan saat menghapus user',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,11 @@ const User = new Elysia({ prefix: "/api/user" })
|
||||
roleId: t.Optional(t.String()),
|
||||
})
|
||||
}
|
||||
);
|
||||
)
|
||||
.put("/delUser/:id", userDelete, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default User;
|
||||
|
||||
@@ -1,7 +1,127 @@
|
||||
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// import prisma from "@/lib/prisma";
|
||||
// import { Context } from "elysia";
|
||||
|
||||
// export default async function userUpdate(context: Context) {
|
||||
// try {
|
||||
// const { id, isActive, roleId } = await context.body as {
|
||||
// id: string,
|
||||
// isActive?: boolean,
|
||||
// roleId?: string
|
||||
// };
|
||||
|
||||
// if (!id) {
|
||||
// return {
|
||||
// success: false,
|
||||
// message: "ID user wajib ada",
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Optional: cek apakah roleId valid
|
||||
// if (roleId) {
|
||||
// const cekRole = await prisma.role.findUnique({
|
||||
// where: { id: roleId }
|
||||
// });
|
||||
|
||||
// if (!cekRole) {
|
||||
// return {
|
||||
// success: false,
|
||||
// message: "Role tidak ditemukan",
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
// // ✅ CEK: Apakah roleId berubah?
|
||||
// let isRoleChanged = false;
|
||||
// let oldRoleId: string | null = null;
|
||||
|
||||
// if (roleId) {
|
||||
// const currentUser = await prisma.user.findUnique({
|
||||
// where: { id },
|
||||
// select: {
|
||||
// roleId: true,
|
||||
// username: true,
|
||||
// }
|
||||
// });
|
||||
|
||||
// if (currentUser && currentUser.roleId !== roleId) {
|
||||
// isRoleChanged = true;
|
||||
// oldRoleId = currentUser.roleId;
|
||||
// console.log(`🔄 Role berubah untuk ${currentUser.username}: ${oldRoleId} → ${roleId}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Update user
|
||||
// const updatedUser = await prisma.user.update({
|
||||
// where: { id },
|
||||
// data: {
|
||||
// ...(isActive !== undefined && { isActive }),
|
||||
// ...(roleId && { roleId }),
|
||||
// },
|
||||
// select: {
|
||||
// id: true,
|
||||
// username: true,
|
||||
// nomor: true,
|
||||
// isActive: true,
|
||||
// roleId: true,
|
||||
// updatedAt: true,
|
||||
// role: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// // ✅ FORCE LOGOUT: Hapus UserSession jika role berubah
|
||||
// if (isRoleChanged) {
|
||||
// try {
|
||||
// const deletedSessions = await prisma.userSession.deleteMany({
|
||||
// where: { userId: id }
|
||||
// });
|
||||
|
||||
// console.log(`🔒 Force logout user ${updatedUser.username} (${id})`);
|
||||
// console.log(` Deleted ${deletedSessions.count} session(s)`);
|
||||
// console.log(` Role: ${oldRoleId} → ${roleId}`);
|
||||
// } catch (sessionError: any) {
|
||||
// // Jika UserSession tidak ditemukan (user belum pernah login), skip error
|
||||
// if (sessionError.code !== 'P2025') {
|
||||
// console.error("⚠️ Error menghapus session:", sessionError);
|
||||
// } else {
|
||||
// console.log(`ℹ️ User ${updatedUser.username} belum pernah login`);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // ✅ Response dengan info tambahan
|
||||
// return {
|
||||
// success: true,
|
||||
// message: isRoleChanged
|
||||
// ? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.`
|
||||
// : "User berhasil diupdate",
|
||||
// data: updatedUser,
|
||||
// roleChanged: isRoleChanged, // Info untuk frontend
|
||||
// oldRoleId: oldRoleId,
|
||||
// newRoleId: roleId,
|
||||
// };
|
||||
|
||||
// } catch (e: any) {
|
||||
// console.error("❌ Error update user:", e);
|
||||
// return {
|
||||
// success: false,
|
||||
// message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
|
||||
// API update user (Elysia atau Next.js API Route)
|
||||
export default async function userUpdate(context: Context) {
|
||||
try {
|
||||
const { id, isActive, roleId } = await context.body as {
|
||||
@@ -17,12 +137,9 @@ export default async function userUpdate(context: Context) {
|
||||
};
|
||||
}
|
||||
|
||||
// Optional: cek apakah roleId valid
|
||||
// Cek apakah roleId valid
|
||||
if (roleId) {
|
||||
const cekRole = await prisma.role.findUnique({
|
||||
where: { id: roleId }
|
||||
});
|
||||
|
||||
const cekRole = await prisma.role.findUnique({ where: { id: roleId } });
|
||||
if (!cekRole) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -31,11 +148,24 @@ export default async function userUpdate(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Deteksi perubahan role
|
||||
let isRoleChanged = false;
|
||||
if (roleId) {
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: { roleId: true }
|
||||
});
|
||||
isRoleChanged = currentUser?.roleId !== roleId;
|
||||
}
|
||||
|
||||
// ✅ UPDATE USER + INVALIDATE SESSION
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(isActive !== undefined && { isActive }),
|
||||
...(roleId && { roleId }),
|
||||
// Force logout: set sessionInvalid = true
|
||||
...(isRoleChanged && { sessionInvalid: true }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -43,27 +173,36 @@ export default async function userUpdate(context: Context) {
|
||||
nomor: true,
|
||||
isActive: true,
|
||||
roleId: true,
|
||||
updatedAt: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
role: { select: { name: true } }
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Reset sessionInvalid setelah 5 detik (opsional)
|
||||
if (isRoleChanged) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { sessionInvalid: false }
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Gagal reset sessionInvalid:', e);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `User berhasil diupdate`,
|
||||
message: isRoleChanged
|
||||
? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.`
|
||||
: "User berhasil diupdate",
|
||||
data: updatedUser,
|
||||
};
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("Error update user:", e);
|
||||
console.error("❌ Error update user:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengupdate user",
|
||||
message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -117,4 +117,44 @@ export const apiFetchVerifyOtp = async ({
|
||||
...data,
|
||||
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' };
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
// app/api/auth/_lib/session_create.ts
|
||||
import { cookies } from "next/headers";
|
||||
import { encrypt } from "./encrypt";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function sessionCreate({
|
||||
sessionKey,
|
||||
exp = "7 year",
|
||||
exp = "30 day",
|
||||
jwtSecret,
|
||||
user,
|
||||
}: {
|
||||
@@ -30,12 +32,59 @@ export async function sessionCreate({
|
||||
throw new Error("Token generation failed");
|
||||
}
|
||||
|
||||
// ✅ HYBRID: Simpan token ke database UserSession
|
||||
const userId = user.id as string;
|
||||
|
||||
if (userId) {
|
||||
try {
|
||||
// Hapus session lama user ini (logout device lain)
|
||||
await prisma.userSession.deleteMany({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
// Parse expiration
|
||||
const expiresDate = new Date();
|
||||
const expMatch = exp.match(/(\d+)\s*(day|year)/);
|
||||
|
||||
if (expMatch) {
|
||||
const [, num, unit] = expMatch;
|
||||
const amount = parseInt(num);
|
||||
|
||||
if (unit === 'year') {
|
||||
expiresDate.setFullYear(expiresDate.getFullYear() + amount);
|
||||
} else if (unit === 'day') {
|
||||
expiresDate.setDate(expiresDate.getDate() + amount);
|
||||
}
|
||||
} else {
|
||||
// Default 30 hari
|
||||
expiresDate.setDate(expiresDate.getDate() + 30);
|
||||
}
|
||||
|
||||
// Buat session baru di database
|
||||
await prisma.userSession.create({
|
||||
data: {
|
||||
userId,
|
||||
token, // JWT token disimpan
|
||||
expires: expiresDate,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Session created for user ${userId}`);
|
||||
} catch (dbError) {
|
||||
console.error("⚠️ Error menyimpan session ke database:", dbError);
|
||||
// Tetap lanjut meski gagal simpan ke DB (fallback ke JWT only)
|
||||
}
|
||||
}
|
||||
|
||||
// Set cookie
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(sessionKey, token, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik
|
||||
});
|
||||
|
||||
return token;
|
||||
|
||||
42
src/app/api/auth/_lib/session_delete.ts
Normal file
42
src/app/api/auth/_lib/session_delete.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// app/api/auth/_lib/session_delete.ts
|
||||
import { cookies } from "next/headers";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
/**
|
||||
* Hapus session dari database dan cookie
|
||||
*/
|
||||
export async function sessionDelete({
|
||||
sessionKey,
|
||||
userId,
|
||||
}: {
|
||||
sessionKey: string;
|
||||
userId?: string;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get(sessionKey)?.value;
|
||||
|
||||
// Hapus dari database
|
||||
if (token) {
|
||||
const deleted = await prisma.userSession.deleteMany({
|
||||
where: { token },
|
||||
});
|
||||
console.log(`🗑️ Deleted ${deleted.count} session(s) by token`);
|
||||
} else if (userId) {
|
||||
// Fallback: hapus berdasarkan userId
|
||||
const deleted = await prisma.userSession.deleteMany({
|
||||
where: { userId },
|
||||
});
|
||||
console.log(`🗑️ Deleted ${deleted.count} session(s) for user ${userId}`);
|
||||
}
|
||||
|
||||
// Hapus cookie
|
||||
cookieStore.delete(sessionKey);
|
||||
console.log('✅ Session deleted successfully');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ Error deleting session:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
136
src/app/api/auth/_lib/session_verify.ts
Normal file
136
src/app/api/auth/_lib/session_verify.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// // app/api/auth/_lib/session_verify.ts
|
||||
// import { cookies } from 'next/headers';
|
||||
// import { decrypt } from './decrypt';
|
||||
// import prisma from '@/lib/prisma';
|
||||
|
||||
// /**
|
||||
// * Verifikasi session hybrid:
|
||||
// * 1. Decrypt JWT token
|
||||
// * 2. Cek apakah token masih ada di database (untuk force logout)
|
||||
// * 3. Return data user terbaru dari database
|
||||
// */
|
||||
// export async function verifySession(): Promise<Record<string, unknown> | null> {
|
||||
// try {
|
||||
// const sessionKey = process.env.BASE_SESSION_KEY;
|
||||
// if (!sessionKey) {
|
||||
// throw new Error('BASE_SESSION_KEY tidak ditemukan di environment');
|
||||
// }
|
||||
|
||||
// const jwtSecret = process.env.BASE_TOKEN_KEY;
|
||||
// if (!jwtSecret) {
|
||||
// throw new Error('BASE_TOKEN_KEY tidak ditemukan di environment');
|
||||
// }
|
||||
|
||||
// const cookieStore = await cookies();
|
||||
// const token = cookieStore.get(sessionKey)?.value;
|
||||
|
||||
// if (!token) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// // Step 1: Decrypt JWT
|
||||
// const jwtUser = await decrypt({ token, jwtSecret });
|
||||
|
||||
// if (!jwtUser || !jwtUser.id) {
|
||||
// console.log('⚠️ JWT decrypt failed atau tidak ada user ID');
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// // Step 2: Cek database UserSession (untuk force logout)
|
||||
// try {
|
||||
// const dbSession = await prisma.userSession.findFirst({
|
||||
// where: {
|
||||
// userId: jwtUser.id as string,
|
||||
// token: token,
|
||||
// active: true,
|
||||
// OR: [
|
||||
// { expires: null },
|
||||
// { expires: { gte: new Date() } },
|
||||
// ],
|
||||
// },
|
||||
// include: {
|
||||
// User: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// username: true,
|
||||
// nomor: true,
|
||||
// roleId: true,
|
||||
// isActive: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Token tidak ditemukan di database = sudah dihapus (force logout)
|
||||
// if (!dbSession) {
|
||||
// console.log('⚠️ Token valid tapi sudah dihapus dari database (force logout)');
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// // Step 3: Return data user terbaru dari database
|
||||
// // Ini penting agar roleId selalu update
|
||||
// return {
|
||||
// id: dbSession.User.id,
|
||||
// username: dbSession.User.username,
|
||||
// nomor: dbSession.User.nomor,
|
||||
// roleId: dbSession.User.roleId,
|
||||
// isActive: dbSession.User.isActive,
|
||||
// };
|
||||
|
||||
// } catch (dbError) {
|
||||
// console.error("⚠️ Error cek database session:", dbError);
|
||||
// // Fallback: jika database error, tetap pakai JWT
|
||||
// return jwtUser;
|
||||
// }
|
||||
|
||||
// } catch (error) {
|
||||
// console.warn('❌ Session verification failed:', error);
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// src/app/api/auth/_lib/session_verify.ts
|
||||
import { cookies } from 'next/headers';
|
||||
import { decrypt } from './decrypt';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function verifySession() {
|
||||
try {
|
||||
const sessionKey = process.env.BASE_SESSION_KEY;
|
||||
const jwtSecret = process.env.BASE_TOKEN_KEY;
|
||||
|
||||
if (!sessionKey || !jwtSecret) {
|
||||
throw new Error('Environment variables tidak lengkap');
|
||||
}
|
||||
|
||||
const token = (await cookies()).get(sessionKey)?.value;
|
||||
if (!token) return null;
|
||||
|
||||
// Decrypt JWT
|
||||
const jwtUser = await decrypt({ token, jwtSecret });
|
||||
if (!jwtUser || !jwtUser.id) return null;
|
||||
|
||||
// ✅ Cek apakah session di-invalidate
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: jwtUser.id as string },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nomor: true,
|
||||
roleId: true,
|
||||
isActive: true,
|
||||
sessionInvalid: true, // ← Tambahkan field ini
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || user.sessionInvalid) {
|
||||
console.log('⚠️ Session tidak valid (force logout)');
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.warn('Session verification failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,99 @@
|
||||
// app/api/auth/finalize-registration/route.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
// import { sessionCreate } from "../_lib/session_create";
|
||||
import { sessionCreate } from "../_lib/session_create";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { nomor, username, kodeId } = await req.json();
|
||||
const { nomor, username, kodeId, roleId } = await req.json();
|
||||
|
||||
// Verifikasi OTP (sama seperti verify-otp)
|
||||
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
|
||||
if (!otpRecord?.isActive || otpRecord.nomor !== nomor) {
|
||||
return NextResponse.json({ success: false, message: 'OTP tidak valid' }, { status: 400 });
|
||||
// Validasi input
|
||||
if (!nomor || !username || !kodeId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Data tidak lengkap" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Buat user
|
||||
const user = await prisma.user.create({
|
||||
data: { username, nomor, isActive: false }
|
||||
// Verifikasi OTP
|
||||
const otpRecord = await prisma.kodeOtp.findUnique({
|
||||
where: { id: kodeId },
|
||||
});
|
||||
|
||||
if (!otpRecord?.isActive || otpRecord.nomor !== nomor) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "OTP tidak valid" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Cek apakah username sudah dipakai
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { username },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Username sudah digunakan" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Buat user baru
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
nomor,
|
||||
roleId: roleId || "1", // Default role
|
||||
isActive: false, // Menunggu approval
|
||||
},
|
||||
});
|
||||
|
||||
// Nonaktifkan OTP
|
||||
await prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } });
|
||||
|
||||
// Buat session
|
||||
// const token = await sessionCreate({
|
||||
// sessionKey: process.env.BASE_SESSION_KEY!,
|
||||
// jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||
// user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true },
|
||||
// });
|
||||
|
||||
(await cookies()).set('desadarmasaba_user_id', user.id, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 hari
|
||||
await prisma.kodeOtp.update({
|
||||
where: { id: kodeId },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
// ✅ CREATE SESSION (JWT + Database)
|
||||
try {
|
||||
await sessionCreate({
|
||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||
exp: "30 day",
|
||||
user: {
|
||||
id: newUser.id,
|
||||
nomor: newUser.nomor,
|
||||
username: newUser.username,
|
||||
roleId: newUser.roleId,
|
||||
isActive: false, // User baru belum aktif
|
||||
},
|
||||
});
|
||||
} catch (sessionError) {
|
||||
console.error("❌ Error creating session:", sessionError);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Gagal membuat session" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Registrasi berhasil. Menunggu persetujuan admin.",
|
||||
user: {
|
||||
id: newUser.id,
|
||||
name: newUser.username,
|
||||
roleId: newUser.roleId,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
const response = NextResponse.json({ success: true, roleId: user.roleId });
|
||||
// response.cookies.set(process.env.BASE_SESSION_KEY!, token, { /* options */ });
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Finalize Registration Error:', error);
|
||||
return NextResponse.json({ success: false, message: 'Registrasi gagal' }, { status: 500 });
|
||||
console.error("❌ Finalize Registration Error:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Registrasi gagal" },
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
35
src/app/api/auth/logout/route.ts
Normal file
35
src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// app/api/auth/logout/route.ts
|
||||
import { NextResponse } from "next/server";
|
||||
import { sessionDelete } from "../_lib/session_delete";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const deleted = await sessionDelete({
|
||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||
});
|
||||
|
||||
if (deleted) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Logout berhasil",
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Gagal logout",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Logout Error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Terjadi kesalahan saat logout",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,41 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { NextRequest } from "next/server";
|
||||
// Jika pakai custom session (bukan next-auth), ganti dengan logic session-mu
|
||||
// src/app/api/auth/me/route.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import { verifySession } from '../_lib/session_verify';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
// 🔸 GANTI DENGAN LOGIC SESSION-MU
|
||||
// Contoh: jika kamu simpan user.id di cookie atau JWT
|
||||
const userId = req.cookies.get("desadarmasaba_user_id")?.value; // sesuaikan
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await verifySession();
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Session tidak valid", user: null },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
// ✅ Ambil menu akses kustom
|
||||
const menuAccess = await prisma.userMenuAccess.findMany({
|
||||
where: { userId: user.id },
|
||||
select: { menuId: true },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
username: user.username,
|
||||
nomor: user.nomor,
|
||||
roleId: user.roleId,
|
||||
isActive: user.isActive,
|
||||
menuIds: menuAccess.map(m => m.menuId), // ✅ tambahkan ini
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Error in /api/auth/me:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Terjadi kesalahan", user: null },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nomor: true,
|
||||
isActive: true,
|
||||
role: { select: { name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return Response.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({ user });
|
||||
}
|
||||
@@ -4,13 +4,6 @@ import { NextResponse } from "next/server";
|
||||
import { sessionCreate } from "../_lib/session_create";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
if (req.method !== "POST") {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Method Not Allowed" },
|
||||
{ status: 405 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { nomor, otp, kodeId } = await req.json();
|
||||
|
||||
@@ -41,7 +34,7 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Pastikan tipe data cocok (OTP di DB = number)
|
||||
// Validasi OTP
|
||||
const receivedOtp = Number(otp);
|
||||
if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
|
||||
return NextResponse.json(
|
||||
@@ -76,26 +69,22 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Akun belum disetujui oleh admin" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Buat session
|
||||
const token = await sessionCreate({
|
||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||
jwtSecret: process.env.BASE_TOKEN_KEY!, // ✅
|
||||
user: {
|
||||
id: user.id,
|
||||
nomor: user.nomor,
|
||||
username: user.username,
|
||||
roleId: user.roleId,
|
||||
isActive: user.isActive,
|
||||
},
|
||||
});
|
||||
if (!token) {
|
||||
// ✅ CREATE SESSION (JWT + Database)
|
||||
try {
|
||||
await sessionCreate({
|
||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||
exp: "30 day",
|
||||
user: {
|
||||
id: user.id,
|
||||
nomor: user.nomor,
|
||||
username: user.username,
|
||||
roleId: user.roleId,
|
||||
isActive: user.isActive,
|
||||
},
|
||||
});
|
||||
} catch (sessionError) {
|
||||
console.error("❌ Error creating session:", sessionError);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Gagal membuat session" },
|
||||
{ status: 500 }
|
||||
@@ -108,34 +97,25 @@ export async function POST(req: Request) {
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
const userData = {
|
||||
id: user.id,
|
||||
name: user.username, // atau user.nama jika ada kolom nama
|
||||
roleId: user.roleId,
|
||||
};
|
||||
|
||||
// Set cookie & respons
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "Berhasil login",
|
||||
user: userData,
|
||||
roleId: user.roleId,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: true, // 🔒 lebih aman
|
||||
maxAge: 30 * 24 * 60 * 60,
|
||||
// Update lastLogin
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLogin: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: user.isActive ? "Berhasil login" : "Menunggu persetujuan",
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
roleId: user.roleId,
|
||||
isActive: user.isActive,
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Verify OTP Error:", error);
|
||||
console.error("❌ Verify OTP Error:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Terjadi kesalahan saat verifikasi" },
|
||||
{ status: 500 }
|
||||
|
||||
@@ -19,37 +19,42 @@ export default function WaitingRoom() {
|
||||
// const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const data = await fetchUser();
|
||||
if (!isMounted) return;
|
||||
|
||||
setUser(data.user);
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const data = await fetchUser();
|
||||
if (!isMounted) return;
|
||||
|
||||
// Jika sudah aktif, redirect ke dashboard admin
|
||||
if (data.user.isActive) {
|
||||
clearInterval(interval);
|
||||
router.push('/admin'); // atau /dashboard
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!isMounted) return;
|
||||
setError(err.message || 'Gagal memuat status');
|
||||
const currentUser = data.user;
|
||||
setUser(currentUser);
|
||||
|
||||
// ✅ Sekarang isActive tersedia!
|
||||
if (currentUser?.isActive) {
|
||||
clearInterval(interval);
|
||||
// Redirect ke login jika unauthorized
|
||||
if (err.message === 'Unauthorized') {
|
||||
router.push('/login');
|
||||
// Redirect ke halaman admin sesuai role
|
||||
if (currentUser.roleId === 0) {
|
||||
router.push('/admin/landing-page/profil/program-inovasi');
|
||||
} else {
|
||||
router.push('/admin'); // atau halaman default role
|
||||
}
|
||||
}
|
||||
}, 2000); // Cek setiap 2 detik
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
isMounted = false;
|
||||
} catch (err: any) {
|
||||
if (!isMounted) return;
|
||||
setError(err.message || 'Gagal memuat status');
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [router]);
|
||||
if (err.message === 'Unauthorized') {
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -1,46 +1,98 @@
|
||||
// app/middleware.js
|
||||
import { NextResponse, NextRequest } from 'next/server';
|
||||
// // app/middleware.js
|
||||
// import { NextResponse, NextRequest } from 'next/server';
|
||||
|
||||
// Daftar route yang diizinkan tanpa login (public routes)
|
||||
const publicRoutes = [
|
||||
'/*', // Home page
|
||||
'/about', // About page
|
||||
'/public/*', // Wildcard untuk semua route di bawah /public
|
||||
'/login', // Halaman login
|
||||
// // Daftar route yang diizinkan tanpa login (public routes)
|
||||
// const publicRoutes = [
|
||||
// '/*', // Home page
|
||||
// '/about', // About page
|
||||
// '/public/*', // Wildcard untuk semua route di bawah /public
|
||||
// '/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
|
||||
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 async function middleware(request: NextRequest) {
|
||||
const path = request.nextUrl.pathname;
|
||||
|
||||
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
|
||||
// Izinkan akses ke route publik di /admin
|
||||
if (PUBLIC_ADMIN_ROUTES.some(route => path.startsWith(route))) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Ambil token dari cookie
|
||||
const token = request.cookies.get(process.env.BASE_SESSION_KEY!)?.value;
|
||||
if (!token) {
|
||||
return NextResponse.redirect(new URL('/login', request.url));
|
||||
}
|
||||
|
||||
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();
|
||||
} 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 = {
|
||||
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';
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
roleId: number; // 0, 1, 2, 3
|
||||
roleId: number;
|
||||
menuIds?: string[] | null; // ✅ Pastikan pakai `string[]`
|
||||
};
|
||||
|
||||
export const authStore = proxy<{
|
||||
|
||||
Reference in New Issue
Block a user