nico/21-jan-26 #50
@@ -90,40 +90,88 @@ const userState = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: {
|
deleteUser: {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
|
async delete(id: string) {
|
||||||
this.loading = true;
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/updt`, {
|
userState.deleteUser.loading = true;
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
const response = await fetch(`/api/user/delUser/${id}`, {
|
||||||
body: JSON.stringify(payload),
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (res.status === 200 && data.success) {
|
if (response.ok && result?.success) {
|
||||||
toast.success(data.message);
|
toast.success(result.message || "User berhasil dihapus permanen");
|
||||||
|
await userState.findMany.load(); // refresh list user setelah delete
|
||||||
// refresh list
|
|
||||||
userState.findMany.load(
|
|
||||||
userState.findMany.page,
|
|
||||||
10,
|
|
||||||
userState.findMany.search
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || "Gagal update user");
|
toast.error(result?.message || "Gagal menghapus user");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error(e);
|
console.error("Gagal delete user:", error);
|
||||||
toast.error("Gagal update user");
|
toast.error("Terjadi kesalahan saat menghapus user");
|
||||||
} finally {
|
} 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({
|
const templateRole = z.object({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// app/validasi/page.tsx
|
//
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { apiFetchOtpData, apiFetchVerifyOtp } from '@/app/api/auth/_lib/api_fetch_auth';
|
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 [isLoading, setIsLoading] = useState(true);
|
||||||
const [kodeId, setKodeId] = useState<string | null>(null);
|
const [kodeId, setKodeId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Inisialisasi data OTP
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedKodeId = localStorage.getItem('auth_kodeId');
|
const storedKodeId = localStorage.getItem('auth_kodeId');
|
||||||
if (!storedKodeId) {
|
if (!storedKodeId) {
|
||||||
toast.error('Akses tidak valid');
|
toast.error('Akses tidak valid');
|
||||||
router.push('/login');
|
router.replace('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setKodeId(storedKodeId);
|
setKodeId(storedKodeId);
|
||||||
|
|
||||||
const fetchOtpData = async () => {
|
const loadOtpData = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await apiFetchOtpData({ kodeId: storedKodeId });
|
const result = await apiFetchOtpData({ kodeId: storedKodeId });
|
||||||
if (result.success && result.data?.nomor) {
|
if (result.success && result.data?.nomor) {
|
||||||
setNomor(result.data.nomor);
|
setNomor(result.data.nomor);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('OTP tidak valid');
|
throw new Error('Data OTP tidak valid');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gagal muat OTP:', error);
|
console.error('Gagal memuat data OTP:', error);
|
||||||
toast.error('Kode verifikasi tidak valid');
|
toast.error('Kode verifikasi tidak valid');
|
||||||
router.push('/login');
|
router.replace('/login');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchOtpData();
|
loadOtpData();
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
|
// Verifikasi OTP
|
||||||
const handleVerify = async () => {
|
const handleVerify = async () => {
|
||||||
if (!kodeId || !nomor || otp.length < 4) return;
|
if (!kodeId || !nomor || otp.length < 4) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
|
||||||
const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId });
|
const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId });
|
||||||
|
|
||||||
if (verifyResult.success && verifyResult.user) {
|
if (!verifyResult.success) {
|
||||||
// ✅ SET USER KE STORE
|
// Registrasi baru?
|
||||||
authStore.setUser({
|
if (
|
||||||
id: verifyResult.user.id,
|
verifyResult.status === 404 &&
|
||||||
name: verifyResult.user.name,
|
verifyResult.message?.includes('Akun tidak ditemukan')
|
||||||
roleId: Number(verifyResult.user.roleId),
|
) {
|
||||||
});
|
await handleNewRegistration();
|
||||||
|
|
||||||
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');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regRes = await fetch('/api/auth/finalize-registration', {
|
// Error lain
|
||||||
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"
|
|
||||||
toast.error(verifyResult.message || 'Verifikasi gagal');
|
toast.error(verifyResult.message || 'Verifikasi gagal');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Verifikasi sukses → simpan user ke store
|
||||||
|
const user = verifyResult.user;
|
||||||
|
console.log('=== DEBUG USER ===');
|
||||||
|
console.log('Full user object:', user);
|
||||||
|
|
||||||
|
if (!user || !user.id) {
|
||||||
|
toast.error('Data pengguna tidak lengkap');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleId = Number(user.roleId);
|
||||||
|
authStore.setUser({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name || user.username || 'User',
|
||||||
|
roleId: roleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanupStorage();
|
||||||
|
|
||||||
|
const isUserActive = user.isActive ?? user.is_active ?? true;
|
||||||
|
|
||||||
|
// Redirect berdasarkan status approval
|
||||||
|
if (!isUserActive) {
|
||||||
|
router.replace('/waiting-room');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Switch statement lebih clean
|
||||||
|
let redirectPath: string;
|
||||||
|
|
||||||
|
switch (roleId) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
redirectPath = '/admin/kesehatan/posyandu';
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
redirectPath = '/admin';
|
||||||
|
console.warn('Unknown roleId:', roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Redirecting to:', redirectPath);
|
||||||
|
router.replace(redirectPath);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Verifikasi error:', error);
|
console.error('Error saat verifikasi:', error);
|
||||||
toast.error('Terjadi kesalahan');
|
toast.error('Terjadi kesalahan sistem');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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 = () => {
|
const cleanupStorage = () => {
|
||||||
localStorage.removeItem('auth_kodeId');
|
localStorage.removeItem('auth_kodeId');
|
||||||
localStorage.removeItem('auth_nomor');
|
localStorage.removeItem('auth_nomor');
|
||||||
@@ -118,12 +181,15 @@ export default function Validasi() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
localStorage.setItem('auth_kodeId', data.kodeId);
|
localStorage.setItem('auth_kodeId', data.kodeId);
|
||||||
toast.success('OTP baru dikirim');
|
toast.success('OTP baru dikirim');
|
||||||
|
} else {
|
||||||
|
toast.error(data.message || 'Gagal mengirim ulang OTP');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal kirim ulang');
|
toast.error('Gagal menghubungi server');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Loading
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
|
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
|
||||||
@@ -148,7 +214,7 @@ export default function Validasi() {
|
|||||||
Kami telah mengirim kode ke nomor <strong>{nomor}</strong>
|
Kami telah mengirim kode ke nomor <strong>{nomor}</strong>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
<Box mb={20}>
|
<Box mb={20}>
|
||||||
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
||||||
Masukkan Kode Verifikasi
|
Masukkan Kode Verifikasi
|
||||||
@@ -176,7 +242,14 @@ export default function Validasi() {
|
|||||||
|
|
||||||
<Text ta="center" size="sm" mt="md">
|
<Text ta="center" size="sm" mt="md">
|
||||||
Tidak menerima kode?{' '}
|
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
|
Kirim Ulang
|
||||||
</Button>
|
</Button>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
import colors from '@/con/colors';
|
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 { 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 { 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 { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../_com/header';
|
import HeaderSearch from '../../_com/header';
|
||||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||||
import user from '../../_state/user/user-state';
|
import user from '../../_state/user/user-state';
|
||||||
|
|
||||||
|
|
||||||
function User() {
|
function User() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
@@ -27,10 +26,10 @@ function User() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListUser({ search }: { search: string }) {
|
function ListUser({ search }: { search: string }) {
|
||||||
const stateUser = useProxy(user.userState)
|
const stateUser = useProxy(user.userState);
|
||||||
const stateRole = useProxy(user.roleState)
|
const stateRole = useProxy(user.roleState);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -42,21 +41,80 @@ function ListUser({ search }: { search: string }) {
|
|||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
stateUser.delete.submit(selectedId)
|
stateUser.deleteUser.delete(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
|
stateUser.findMany.load();
|
||||||
stateUser.findMany.load()
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
stateRole.findMany.load()
|
stateRole.findMany.load();
|
||||||
load(page, 10, search)
|
load(page, 10, search);
|
||||||
}, [page, 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) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
@@ -80,16 +138,19 @@ function ListUser({ search }: { search: string }) {
|
|||||||
<TableTh style={{ width: '20%' }}>Nomor</TableTh>
|
<TableTh style={{ width: '20%' }}>Nomor</TableTh>
|
||||||
<TableTh style={{ width: '20%' }}>Role</TableTh>
|
<TableTh style={{ width: '20%' }}>Role</TableTh>
|
||||||
<TableTh style={{ width: '15%' }}>Aktif / Nonaktif</TableTh>
|
<TableTh style={{ width: '15%' }}>Aktif / Nonaktif</TableTh>
|
||||||
|
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '25%', }}>
|
<TableTd style={{ width: '25%' }}>
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>{item.username}</Text>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
|
{item.username}
|
||||||
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%', }}>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<Text truncate fz="sm" c="dimmed">
|
<Text truncate fz="sm" c="dimmed">
|
||||||
{item.nomor}
|
{item.nomor}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -101,21 +162,22 @@ function ListUser({ search }: { search: string }) {
|
|||||||
label: r.name,
|
label: r.name,
|
||||||
value: r.id,
|
value: r.id,
|
||||||
}))}
|
}))}
|
||||||
value={item.roleId} // ⬅ role milik user ini
|
value={item.roleId}
|
||||||
onChange={async (val) => {
|
onChange={(val) => {
|
||||||
if (!val) return;
|
if (!val) return;
|
||||||
|
|
||||||
await stateUser.update.submit({
|
// ✅ Panggil handleRoleChange dengan konfirmasi
|
||||||
id: item.id,
|
handleRoleChange(
|
||||||
roleId: val, // ⬅ kirim roleId
|
item.id,
|
||||||
});
|
item.username,
|
||||||
|
item.roleId,
|
||||||
// reload data supaya UI up-to-date
|
val
|
||||||
stateUser.findMany.load(page, 10, search);
|
);
|
||||||
}}
|
}}
|
||||||
searchable
|
searchable
|
||||||
clearable={false} // role harus ada
|
clearable={false}
|
||||||
nothingFoundMessage="Role tidak ditemukan"
|
nothingFoundMessage="Role tidak ditemukan"
|
||||||
|
disabled={stateUser.update.loading}
|
||||||
/>
|
/>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
|
||||||
@@ -127,26 +189,34 @@ function ListUser({ search }: { search: string }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color={item.isActive ? "green" : "red"}
|
color={item.isActive ? "green" : "red"}
|
||||||
onClick={async () => {
|
onClick={() => handleToggleActive(item.id, item.isActive)}
|
||||||
await stateUser.update.submit({
|
disabled={stateUser.update.loading}
|
||||||
id: item.id,
|
|
||||||
isActive: !item.isActive, // toggle
|
|
||||||
});
|
|
||||||
|
|
||||||
stateUser.findMany.load(page, 10, search);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{item.isActive ? <IconCheck size={20} /> : <IconX size={20} />}
|
{item.isActive ? <IconCheck size={20} /> : <IconX size={20} />}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TableTd>
|
</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>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4}>
|
<TableTd colSpan={5}>
|
||||||
<Center py={20}>
|
<Center py={20}>
|
||||||
<Text color="dimmed">Tidak ada data user yang cocok</Text>
|
<Text c="dimmed">Tidak ada data user yang cocok</Text>
|
||||||
</Center>
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
@@ -155,6 +225,7 @@ function ListUser({ search }: { search: string }) {
|
|||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
@@ -169,6 +240,7 @@ function ListUser({ search }: { search: string }) {
|
|||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// /* eslint-disable react-hooks/exhaustive-deps */
|
||||||
// 'use client'
|
// 'use client'
|
||||||
|
|
||||||
// import colors from "@/con/colors";
|
// import colors from "@/con/colors";
|
||||||
@@ -257,9 +258,6 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
@@ -271,9 +269,11 @@ import {
|
|||||||
AppShellMain,
|
AppShellMain,
|
||||||
AppShellNavbar,
|
AppShellNavbar,
|
||||||
Burger,
|
Burger,
|
||||||
|
Center,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
|
Loader,
|
||||||
NavLink,
|
NavLink,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Text,
|
Text,
|
||||||
@@ -289,23 +289,164 @@ import {
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||||
import { useSnapshot } from "valtio";
|
import { useEffect, useState } from "react";
|
||||||
import { navigationByRole } from "./_com/navigationByRole";
|
import { navigationByRole } from "./_com/navigationByRole";
|
||||||
|
import { useSnapshot } from "valtio";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
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 [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||||
|
|
||||||
//ambil user dari authStore
|
const { user } = useSnapshot(authStore);
|
||||||
const {user} = useSnapshot(authStore)
|
console.log("Current user in store:", user);
|
||||||
console.log("Current User:", user); // 👈 Tambahkan ini
|
|
||||||
|
|
||||||
//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
|
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 (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
@@ -404,9 +545,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={handleLogout}
|
||||||
router.push("/darmasaba");
|
|
||||||
}}
|
|
||||||
color={colors["blue-button"]}
|
color={colors["blue-button"]}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="lg"
|
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()),
|
roleId: t.Optional(t.String()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
.put("/delUser/:id", userDelete, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export default User;
|
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({
|
const updatedUser = await prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `User berhasil diupdate`,
|
message: isRoleChanged
|
||||||
|
? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.`
|
||||||
|
: "User berhasil diupdate",
|
||||||
data: updatedUser,
|
data: updatedUser,
|
||||||
|
roleChanged: isRoleChanged, // Info untuk frontend
|
||||||
|
oldRoleId: oldRoleId,
|
||||||
|
newRoleId: roleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error update user:", e);
|
console.error("❌ Error update user:", e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
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 { cookies } from "next/headers";
|
||||||
import { encrypt } from "./encrypt";
|
import { encrypt } from "./encrypt";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
export async function sessionCreate({
|
export async function sessionCreate({
|
||||||
sessionKey,
|
sessionKey,
|
||||||
exp = "7 year",
|
exp = "30 day",
|
||||||
jwtSecret,
|
jwtSecret,
|
||||||
user,
|
user,
|
||||||
}: {
|
}: {
|
||||||
@@ -30,12 +32,59 @@ export async function sessionCreate({
|
|||||||
throw new Error("Token generation failed");
|
throw new Error("Token generation failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ HYBRID: Simpan token ke database UserSession
|
||||||
|
const userId = user.id as string;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
try {
|
||||||
|
// Hapus session lama user ini (logout device lain)
|
||||||
|
await prisma.userSession.deleteMany({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse expiration
|
||||||
|
const expiresDate = new Date();
|
||||||
|
const expMatch = exp.match(/(\d+)\s*(day|year)/);
|
||||||
|
|
||||||
|
if (expMatch) {
|
||||||
|
const [, num, unit] = expMatch;
|
||||||
|
const amount = parseInt(num);
|
||||||
|
|
||||||
|
if (unit === 'year') {
|
||||||
|
expiresDate.setFullYear(expiresDate.getFullYear() + amount);
|
||||||
|
} else if (unit === 'day') {
|
||||||
|
expiresDate.setDate(expiresDate.getDate() + amount);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default 30 hari
|
||||||
|
expiresDate.setDate(expiresDate.getDate() + 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat session baru di database
|
||||||
|
await prisma.userSession.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
token, // JWT token disimpan
|
||||||
|
expires: expiresDate,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Session created for user ${userId}`);
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error("⚠️ Error menyimpan session ke database:", dbError);
|
||||||
|
// Tetap lanjut meski gagal simpan ke DB (fallback ke JWT only)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cookie
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
cookieStore.set(sessionKey, token, {
|
cookieStore.set(sessionKey, token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
path: "/",
|
path: "/",
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik
|
||||||
});
|
});
|
||||||
|
|
||||||
return token;
|
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
|
// app/api/auth/finalize-registration/route.ts
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
// import { sessionCreate } from "../_lib/session_create";
|
import { sessionCreate } from "../_lib/session_create";
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
const { nomor, username, kodeId } = await req.json();
|
const { nomor, username, kodeId, roleId } = await req.json();
|
||||||
|
|
||||||
// Verifikasi OTP (sama seperti verify-otp)
|
// Validasi input
|
||||||
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
|
if (!nomor || !username || !kodeId) {
|
||||||
if (!otpRecord?.isActive || otpRecord.nomor !== nomor) {
|
return NextResponse.json(
|
||||||
return NextResponse.json({ success: false, message: 'OTP tidak valid' }, { status: 400 });
|
{ success: false, message: "Data tidak lengkap" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buat user
|
// Verifikasi OTP
|
||||||
const user = await prisma.user.create({
|
const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
data: { username, nomor, isActive: false }
|
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
|
// Nonaktifkan OTP
|
||||||
await prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } });
|
await prisma.kodeOtp.update({
|
||||||
|
where: { id: kodeId },
|
||||||
// Buat session
|
data: { isActive: false },
|
||||||
// const token = await sessionCreate({
|
});
|
||||||
// sessionKey: process.env.BASE_SESSION_KEY!,
|
|
||||||
// jwtSecret: process.env.BASE_TOKEN_KEY!,
|
// ✅ CREATE SESSION (JWT + Database)
|
||||||
// user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true },
|
try {
|
||||||
// });
|
await sessionCreate({
|
||||||
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
(await cookies()).set('desadarmasaba_user_id', user.id, {
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
httpOnly: true,
|
exp: "30 day",
|
||||||
secure: process.env.NODE_ENV === 'production',
|
user: {
|
||||||
path: '/',
|
id: newUser.id,
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 hari
|
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) {
|
} catch (error) {
|
||||||
console.error('Finalize Registration Error:', error);
|
console.error("❌ Finalize Registration Error:", error);
|
||||||
return NextResponse.json({ success: false, message: 'Registrasi gagal' }, { status: 500 });
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Registrasi gagal" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await prisma.$disconnect();
|
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";
|
// app/api/auth/me/route.ts
|
||||||
import { NextRequest } from "next/server";
|
import { NextResponse } from 'next/server';
|
||||||
// Jika pakai custom session (bukan next-auth), ganti dengan logic session-mu
|
import { verifySession } from '../_lib/session_verify';
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET() {
|
||||||
// 🔸 GANTI DENGAN LOGIC SESSION-MU
|
try {
|
||||||
// Contoh: jika kamu simpan user.id di cookie atau JWT
|
// ✅ Verify session (hybrid: JWT + Database)
|
||||||
const userId = req.cookies.get("desadarmasaba_user_id")?.value; // sesuaikan
|
const user = await verifySession();
|
||||||
|
|
||||||
if (!userId) {
|
if (!user) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
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";
|
import { sessionCreate } from "../_lib/session_create";
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
if (req.method !== "POST") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, message: "Method Not Allowed" },
|
|
||||||
{ status: 405 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { nomor, otp, kodeId } = await req.json();
|
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);
|
const receivedOtp = Number(otp);
|
||||||
if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
|
if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -76,26 +69,22 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.isActive) {
|
// ✅ CREATE SESSION (JWT + Database)
|
||||||
return NextResponse.json(
|
try {
|
||||||
{ success: false, message: "Akun belum disetujui oleh admin" },
|
await sessionCreate({
|
||||||
{ status: 403 }
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
);
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
}
|
exp: "30 day",
|
||||||
|
user: {
|
||||||
// Buat session
|
id: user.id,
|
||||||
const token = await sessionCreate({
|
nomor: user.nomor,
|
||||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
username: user.username,
|
||||||
jwtSecret: process.env.BASE_TOKEN_KEY!, // ✅
|
roleId: user.roleId,
|
||||||
user: {
|
isActive: user.isActive,
|
||||||
id: user.id,
|
},
|
||||||
nomor: user.nomor,
|
});
|
||||||
username: user.username,
|
} catch (sessionError) {
|
||||||
roleId: user.roleId,
|
console.error("❌ Error creating session:", sessionError);
|
||||||
isActive: user.isActive,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!token) {
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Gagal membuat session" },
|
{ success: false, message: "Gagal membuat session" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -108,34 +97,25 @@ export async function POST(req: Request) {
|
|||||||
data: { isActive: false },
|
data: { isActive: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const userData = {
|
// Update lastLogin
|
||||||
id: user.id,
|
await prisma.user.update({
|
||||||
name: user.username, // atau user.nama jika ada kolom nama
|
where: { id: user.id },
|
||||||
roleId: user.roleId,
|
data: { lastLogin: new Date() },
|
||||||
};
|
});
|
||||||
|
|
||||||
// Set cookie & respons
|
return NextResponse.json({
|
||||||
const response = NextResponse.json(
|
success: true,
|
||||||
{
|
message: user.isActive ? "Berhasil login" : "Menunggu persetujuan",
|
||||||
success: true,
|
user: {
|
||||||
message: "Berhasil login",
|
id: user.id,
|
||||||
user: userData,
|
name: user.username,
|
||||||
roleId: user.roleId,
|
roleId: user.roleId,
|
||||||
},
|
isActive: user.isActive,
|
||||||
{ 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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Verify OTP Error:", error);
|
console.error("❌ Verify OTP Error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Terjadi kesalahan saat verifikasi" },
|
{ success: false, message: "Terjadi kesalahan saat verifikasi" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -19,37 +19,42 @@ export default function WaitingRoom() {
|
|||||||
// const [loading, setLoading] = useState(true);
|
// const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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
|
const currentUser = data.user;
|
||||||
if (data.user.isActive) {
|
setUser(currentUser);
|
||||||
clearInterval(interval);
|
|
||||||
router.push('/admin'); // atau /dashboard
|
// ✅ Sekarang isActive tersedia!
|
||||||
}
|
if (currentUser?.isActive) {
|
||||||
} catch (err: any) {
|
|
||||||
if (!isMounted) return;
|
|
||||||
setError(err.message || 'Gagal memuat status');
|
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
// Redirect ke login jika unauthorized
|
// Redirect ke halaman admin sesuai role
|
||||||
if (err.message === 'Unauthorized') {
|
if (currentUser.roleId === 0) {
|
||||||
router.push('/login');
|
router.push('/admin/landing-page/profil/program-inovasi');
|
||||||
|
} else {
|
||||||
|
router.push('/admin'); // atau halaman default role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 2000); // Cek setiap 2 detik
|
} catch (err: any) {
|
||||||
|
if (!isMounted) return;
|
||||||
// Cleanup
|
setError(err.message || 'Gagal memuat status');
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
if (err.message === 'Unauthorized') {
|
||||||
}, [router]);
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { proxy } from 'valtio';
|
|||||||
export type User = {
|
export type User = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
roleId: number; // 0, 1, 2, 3
|
roleId?: number; // 0, 1, 2, 3
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authStore = proxy<{
|
export const authStore = proxy<{
|
||||||
|
|||||||
Reference in New Issue
Block a user