Tampilan Layout sudah sesuai dengan roleIdnya
Sudah sessionnya Sudah disesuaikan juga semisal superadmin ngubah role admin, maka admin tersebut akan logOut dan diarahkan ke halama login sudah bisa logOut
This commit is contained in:
@@ -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,4 +1,5 @@
|
||||
// app/validasi/page.tsx
|
||||
//
|
||||
|
||||
'use client';
|
||||
|
||||
import { apiFetchOtpData, apiFetchVerifyOtp } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||
@@ -17,89 +18,151 @@ export default function Validasi() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [kodeId, setKodeId] = useState<string | null>(null);
|
||||
|
||||
// Inisialisasi data OTP
|
||||
useEffect(() => {
|
||||
const storedKodeId = localStorage.getItem('auth_kodeId');
|
||||
if (!storedKodeId) {
|
||||
toast.error('Akses tidak valid');
|
||||
router.push('/login');
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setKodeId(storedKodeId);
|
||||
|
||||
const fetchOtpData = async () => {
|
||||
const loadOtpData = async () => {
|
||||
try {
|
||||
const result = await apiFetchOtpData({ kodeId: storedKodeId });
|
||||
if (result.success && result.data?.nomor) {
|
||||
setNomor(result.data.nomor);
|
||||
} else {
|
||||
throw new Error('OTP tidak valid');
|
||||
throw new Error('Data OTP tidak valid');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Gagal muat OTP:', error);
|
||||
console.error('Gagal memuat data OTP:', error);
|
||||
toast.error('Kode verifikasi tidak valid');
|
||||
router.push('/login');
|
||||
router.replace('/login');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchOtpData();
|
||||
loadOtpData();
|
||||
}, [router]);
|
||||
|
||||
// Verifikasi OTP
|
||||
const handleVerify = async () => {
|
||||
if (!kodeId || !nomor || otp.length < 4) return;
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId });
|
||||
|
||||
if (verifyResult.success && 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,
|
||||
});
|
||||
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 +181,15 @@ export default function Validasi() {
|
||||
if (data.success) {
|
||||
localStorage.setItem('auth_kodeId', data.kodeId);
|
||||
toast.success('OTP baru dikirim');
|
||||
} else {
|
||||
toast.error(data.message || 'Gagal mengirim ulang OTP');
|
||||
}
|
||||
} catch {
|
||||
toast.error('Gagal kirim ulang');
|
||||
toast.error('Gagal menghubungi server');
|
||||
}
|
||||
};
|
||||
|
||||
// Loading
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
|
||||
@@ -148,7 +214,7 @@ export default function Validasi() {
|
||||
Kami telah mengirim kode ke nomor <strong>{nomor}</strong>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box w="100%">
|
||||
<Box mb={20}>
|
||||
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
||||
Masukkan Kode Verifikasi
|
||||
@@ -176,7 +242,14 @@ export default function Validasi() {
|
||||
|
||||
<Text ta="center" size="sm" mt="md">
|
||||
Tidak menerima kode?{' '}
|
||||
<Button variant="subtle" onClick={handleResend} size="xs" p={0} h="auto" color={colors['blue-button']}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={handleResend}
|
||||
size="xs"
|
||||
p={0}
|
||||
h="auto"
|
||||
color={colors['blue-button']}
|
||||
>
|
||||
Kirim Ulang
|
||||
</Button>
|
||||
</Text>
|
||||
|
||||
@@ -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,80 @@ function ListUser({ search }: { search: string }) {
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
stateUser.delete.submit(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
|
||||
stateUser.findMany.load()
|
||||
stateUser.deleteUser.delete(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
stateUser.findMany.load();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
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) {
|
||||
// Reload data setelah berhasil update
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
}
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
// ✅ Handler untuk toggle isActive
|
||||
const handleToggleActive = async (userId: string, currentStatus: boolean) => {
|
||||
const success = await stateUser.update.submit({
|
||||
id: userId,
|
||||
isActive: !currentStatus,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
@@ -80,16 +138,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>
|
||||
@@ -101,21 +162,22 @@ function ListUser({ search }: { search: string }) {
|
||||
label: r.name,
|
||||
value: r.id,
|
||||
}))}
|
||||
value={item.roleId} // ⬅ role milik user ini
|
||||
onChange={async (val) => {
|
||||
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 +189,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 +225,7 @@ function ListUser({ search }: { search: string }) {
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -169,6 +240,7 @@ function ListUser({ search }: { search: string }) {
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
@@ -180,4 +252,4 @@ function ListUser({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default User;
|
||||
export default User;
|
||||
@@ -1,3 +1,4 @@
|
||||
// /* eslint-disable react-hooks/exhaustive-deps */
|
||||
// 'use client'
|
||||
|
||||
// import colors from "@/con/colors";
|
||||
@@ -257,9 +258,6 @@
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'use client'
|
||||
|
||||
import colors from "@/con/colors";
|
||||
@@ -271,9 +269,11 @@ import {
|
||||
AppShellMain,
|
||||
AppShellNavbar,
|
||||
Burger,
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
NavLink,
|
||||
ScrollArea,
|
||||
Text,
|
||||
@@ -289,23 +289,164 @@ import {
|
||||
import _ from "lodash";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { useEffect, useState } from "react";
|
||||
import { navigationByRole } from "./_com/navigationByRole";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
const router = useRouter();
|
||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||
|
||||
//ambil user dari authStore
|
||||
const {user} = useSnapshot(authStore)
|
||||
console.log("Current User:", user); // 👈 Tambahkan ini
|
||||
const { user } = useSnapshot(authStore);
|
||||
console.log("Current user in store:", user);
|
||||
|
||||
//ambil navigation berdasarkan role
|
||||
// ✅ Fetch user dari backend jika belum ada di store
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setLoading(false);
|
||||
return;
|
||||
} // Sudah ada → jangan fetch
|
||||
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.user) {
|
||||
authStore.setUser({
|
||||
id: data.user.id,
|
||||
name: data.user.name,
|
||||
roleId: Number(data.user.roleId),
|
||||
});
|
||||
} else {
|
||||
authStore.setUser(null);
|
||||
router.replace('/login');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data pengguna:', error);
|
||||
authStore.setUser(null);
|
||||
router.replace('/login');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, [user, router]); // ✅ Sekarang 'user' terdefinisi
|
||||
|
||||
// ✅ Polling untuk cek perubahan role setiap 30 detik
|
||||
// Di layout.tsx - useEffect polling
|
||||
useEffect(() => {
|
||||
if (!user?.id) return;
|
||||
|
||||
const checkRoleUpdate = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me');
|
||||
const data = await res.json();
|
||||
|
||||
// ✅ Session tidak valid (sudah dihapus karena role berubah)
|
||||
if (!data.success || !data.user) {
|
||||
console.log('⚠️ Session tidak valid, logout...');
|
||||
authStore.setUser(null);
|
||||
|
||||
// Clear cookie manual (backup)
|
||||
document.cookie = `${process.env.NEXT_PUBLIC_SESSION_KEY}=; Max-Age=0; path=/;`;
|
||||
|
||||
toast.info('Role Anda telah diubah. Silakan login kembali.', {
|
||||
autoClose: 5000,
|
||||
});
|
||||
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Cek perubahan roleId (seharusnya tidak sampai sini jika session dihapus)
|
||||
const currentRoleId = Number(data.user.roleId);
|
||||
|
||||
if (currentRoleId !== user.roleId) {
|
||||
console.log('🔄 Role berubah! Dari', user.roleId, 'ke', currentRoleId);
|
||||
|
||||
// Update store
|
||||
authStore.setUser({
|
||||
id: data.user.id,
|
||||
name: data.user.name || data.user.username,
|
||||
roleId: currentRoleId,
|
||||
});
|
||||
|
||||
// Redirect ke halaman default role baru
|
||||
let redirectPath = '/admin';
|
||||
|
||||
switch (currentRoleId) {
|
||||
case 0:
|
||||
case 1:
|
||||
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
||||
break;
|
||||
case 2:
|
||||
redirectPath = '/admin/kesehatan/posyandu';
|
||||
break;
|
||||
case 3:
|
||||
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
break;
|
||||
}
|
||||
|
||||
toast.info('Role Anda telah diubah. Mengalihkan halaman...', {
|
||||
autoClose: 5000,
|
||||
});
|
||||
|
||||
router.push(redirectPath);
|
||||
|
||||
// Reload untuk clear semua state
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error checking role update:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Polling setiap 10 detik (lebih responsif dari 30 detik)
|
||||
const interval = setInterval(checkRoleUpdate, 10000);
|
||||
|
||||
// Juga cek saat window focus (user kembali ke tab)
|
||||
const handleFocus = () => {
|
||||
checkRoleUpdate();
|
||||
};
|
||||
|
||||
window.addEventListener('focus', handleFocus);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
};
|
||||
}, [user, router]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<AppShell>
|
||||
<AppShellMain>
|
||||
<Center h="100vh">
|
||||
<Loader />
|
||||
</Center>
|
||||
</AppShellMain>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Ambil menu berdasarkan roleId
|
||||
const currentNav = user?.roleId !== undefined
|
||||
? navigationByRole[user.roleId as keyof typeof navigationByRole] || []
|
||||
: [];
|
||||
? (navigationByRole[user.roleId as keyof typeof navigationByRole] || [])
|
||||
: [];
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.setUser(null);
|
||||
document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`;
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
@@ -388,13 +529,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
variant="gradient"
|
||||
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
>
|
||||
<Image
|
||||
src="/assets/images/darmasaba-icon.png"
|
||||
alt="Logo Darmasaba"
|
||||
w={20}
|
||||
h={20}
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
<Image
|
||||
src="/assets/images/darmasaba-icon.png"
|
||||
alt="Logo Darmasaba"
|
||||
w={20}
|
||||
h={20}
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
style={{
|
||||
minWidth: '20px',
|
||||
height: 'auto',
|
||||
@@ -404,9 +545,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</Tooltip>
|
||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
router.push("/darmasaba");
|
||||
}}
|
||||
onClick={handleLogout}
|
||||
color={colors["blue-button"]}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
|
||||
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;
|
||||
|
||||
@@ -31,6 +31,27 @@ export default async function userUpdate(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 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: {
|
||||
@@ -53,17 +74,43 @@ export default async function userUpdate(context: Context) {
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ FORCE LOGOUT: Hapus UserSession jika role berubah
|
||||
if (isRoleChanged) {
|
||||
try {
|
||||
const deletedSessions = await prisma.userSession.deleteMany({
|
||||
where: { userId: id }
|
||||
});
|
||||
|
||||
console.log(`🔒 Force logout user ${updatedUser.username} (${id})`);
|
||||
console.log(` Deleted ${deletedSessions.count} session(s)`);
|
||||
console.log(` Role: ${oldRoleId} → ${roleId}`);
|
||||
} catch (sessionError: any) {
|
||||
// Jika UserSession tidak ditemukan (user belum pernah login), skip error
|
||||
if (sessionError.code !== 'P2025') {
|
||||
console.error("⚠️ Error menghapus session:", sessionError);
|
||||
} else {
|
||||
console.log(`ℹ️ User ${updatedUser.username} belum pernah login`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Response dengan info tambahan
|
||||
return {
|
||||
success: true,
|
||||
message: `User berhasil diupdate`,
|
||||
message: isRoleChanged
|
||||
? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.`
|
||||
: "User berhasil diupdate",
|
||||
data: updatedUser,
|
||||
roleChanged: isRoleChanged, // Info untuk frontend
|
||||
oldRoleId: oldRoleId,
|
||||
newRoleId: roleId,
|
||||
};
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("Error update user:", e);
|
||||
console.error("❌ Error update user:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengupdate user",
|
||||
message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
90
src/app/api/auth/_lib/session_verify.ts
Normal file
90
src/app/api/auth/_lib/session_verify.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// app/api/auth/_lib/session_verify.ts
|
||||
import { cookies } from 'next/headers';
|
||||
import { decrypt } from './decrypt';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
/**
|
||||
* Verifikasi session hybrid:
|
||||
* 1. Decrypt JWT token
|
||||
* 2. Cek apakah token masih ada di database (untuk force logout)
|
||||
* 3. Return data user terbaru dari database
|
||||
*/
|
||||
export async function verifySession(): Promise<Record<string, unknown> | null> {
|
||||
try {
|
||||
const sessionKey = process.env.BASE_SESSION_KEY;
|
||||
if (!sessionKey) {
|
||||
throw new Error('BASE_SESSION_KEY tidak ditemukan di environment');
|
||||
}
|
||||
|
||||
const jwtSecret = process.env.BASE_TOKEN_KEY;
|
||||
if (!jwtSecret) {
|
||||
throw new Error('BASE_TOKEN_KEY tidak ditemukan di environment');
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get(sessionKey)?.value;
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 1: Decrypt JWT
|
||||
const jwtUser = await decrypt({ token, jwtSecret });
|
||||
|
||||
if (!jwtUser || !jwtUser.id) {
|
||||
console.log('⚠️ JWT decrypt failed atau tidak ada user ID');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 2: Cek database UserSession (untuk force logout)
|
||||
try {
|
||||
const dbSession = await prisma.userSession.findFirst({
|
||||
where: {
|
||||
userId: jwtUser.id as string,
|
||||
token: token,
|
||||
active: true,
|
||||
OR: [
|
||||
{ expires: null },
|
||||
{ expires: { gte: new Date() } },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
User: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nomor: true,
|
||||
roleId: true,
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Token tidak ditemukan di database = sudah dihapus (force logout)
|
||||
if (!dbSession) {
|
||||
console.log('⚠️ Token valid tapi sudah dihapus dari database (force logout)');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 3: Return data user terbaru dari database
|
||||
// Ini penting agar roleId selalu update
|
||||
return {
|
||||
id: dbSession.User.id,
|
||||
username: dbSession.User.username,
|
||||
nomor: dbSession.User.nomor,
|
||||
roleId: dbSession.User.roleId,
|
||||
isActive: dbSession.User.isActive,
|
||||
};
|
||||
|
||||
} catch (dbError) {
|
||||
console.error("⚠️ Error cek database session:", dbError);
|
||||
// Fallback: jika database error, tetap pakai JWT
|
||||
return jwtUser;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('❌ Session verification failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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,45 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { NextRequest } from "next/server";
|
||||
// Jika pakai custom session (bukan next-auth), ganti dengan logic session-mu
|
||||
// app/api/auth/me/route.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import { verifySession } from '../_lib/session_verify';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
// 🔸 GANTI DENGAN LOGIC SESSION-MU
|
||||
// Contoh: jika kamu simpan user.id di cookie atau JWT
|
||||
const userId = req.cookies.get("desadarmasaba_user_id")?.value; // sesuaikan
|
||||
export async function GET() {
|
||||
try {
|
||||
// ✅ Verify session (hybrid: JWT + Database)
|
||||
const user = await verifySession();
|
||||
|
||||
if (!userId) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Session tidak valid",
|
||||
user: null
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Data user sudah fresh dari database (via verifySession)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
username: user.username,
|
||||
nomor: user.nomor,
|
||||
roleId: user.roleId,
|
||||
isActive: user.isActive,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Error in /api/auth/me:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Terjadi kesalahan",
|
||||
user: null
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nomor: true,
|
||||
isActive: true,
|
||||
role: { select: { name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return Response.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({ user });
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { proxy } from 'valtio';
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
roleId: number; // 0, 1, 2, 3
|
||||
roleId?: number; // 0, 1, 2, 3
|
||||
};
|
||||
|
||||
export const authStore = proxy<{
|
||||
|
||||
Reference in New Issue
Block a user