User yang sudah registrasi sudah langsung diarahkan ke layout sesuai dengan roleIdnya

Superadmin sudah bisa menambah atau mengurangkan menu pad user yang diinginkan
Next-------------------------------
Ada bug saat tampilan menu sudah di edit superamin berhasil namun saat user logout tampilan menunya balik ke sebelumnya
This commit is contained in:
2025-11-26 10:14:05 +08:00
parent e30b27f7a4
commit 2fb3666e57
8 changed files with 239 additions and 157 deletions

View File

@@ -82,64 +82,48 @@ export default function Validasi() {
}
};
// ✅ Verifikasi OTP untuk REGISTRASI
const handleRegistrationVerification = async () => {
const username = localStorage.getItem('auth_username');
if (!username) {
toast.error('Data registrasi tidak ditemukan. Silakan ulangi dari awal.');
return;
}
const handleRegistrationVerification = async () => {
const username = localStorage.getItem('auth_username');
if (!username) {
toast.error('Data registrasi tidak ditemukan.');
return;
}
// ✅ Validasi format
const cleanNomor = nomor?.replace(/\D/g, '') ?? '';
if (cleanNomor.length < 10) {
toast.error('Nomor tidak valid');
return;
}
const cleanNomor = nomor?.replace(/\D/g, '') ?? '';
if (cleanNomor.length < 10 || username.trim().length < 5) {
toast.error('Data tidak valid');
return;
}
if (username.trim().length < 5) {
toast.error('Username minimal 5 karakter');
return;
}
// Verifikasi OTP dulu
const verifyRes = await fetch('/api/auth/verify-otp-register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
});
// 1. Verifikasi OTP via endpoint register
const verifyRes = await fetch('/api/auth/verify-otp-register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
});
const verifyData = await verifyRes.json();
if (!verifyRes.ok) {
toast.error(verifyData.message || 'Verifikasi OTP gagal');
return;
}
const verifyData = await verifyRes.json();
// ✅ Kirim ke finalize-registration → akan redirect ke /waiting-room
const finalizeRes = await fetch('/api/auth/finalize-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nomor, username, kodeId }),
credentials: 'include'
});
if (!verifyRes.ok) {
toast.error(verifyData.message || 'Verifikasi OTP gagal');
return;
}
// 2. Finalisasi registrasi
const finalizeRes = await fetch('/api/auth/finalize-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nomor, username, kodeId }), // 🔴 Tidak perlu kirim `otp` ke sini
});
const finalizeData = await finalizeRes.json();
if (!finalizeRes.ok) {
toast.error(finalizeData.message || 'Registrasi gagal');
return;
}
// 3. Set user & redirect
authStore.setUser({
id: finalizeData.user.id,
name: finalizeData.user.name,
roleId: Number(finalizeData.user.roleId),
});
cleanupStorage();
window.location.href = '/waiting-room';
};
if (finalizeRes.redirected) {
// ✅ Redirect otomatis oleh server
window.location.href = finalizeRes.url;
} else {
const data = await finalizeRes.json();
toast.error(data.message || 'Registrasi gagal');
}
};
// ✅ Verifikasi OTP untuk LOGIN
const handleLoginVerification = async () => {

View File

@@ -50,38 +50,45 @@ export default function Layout({ children }: { children: React.ReactNode }) {
return;
}
const fetchUser = async () => {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
const fetchUser = async () => {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
if (data.user) {
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
const menuData = await menuRes.json();
// ✅ Clone ke array mutable
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
? [...menuData.menuIds] // Converts readonly array to mutable
: null;
authStore.setUser({
id: data.user.id,
name: data.user.name,
roleId: Number(data.user.roleId),
menuIds,
});
} else {
authStore.setUser(null);
router.replace('/login');
}
} catch (error) {
console.error('Gagal memuat data pengguna:', error);
if (data.user) {
// Check if user is active
if (!data.user.isActive) {
authStore.setUser(null);
router.replace('/login');
} finally {
setLoading(false);
router.replace('/waiting-room');
return;
}
};
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
const menuData = await menuRes.json();
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
? [...menuData.menuIds]
: null;
authStore.setUser({
id: data.user.id,
name: data.user.name,
roleId: Number(data.user.roleId),
menuIds,
isActive: data.user.isActive // Add isActive to store
});
} else {
authStore.setUser(null);
router.replace('/login');
}
} catch (error) {
console.error('Gagal memuat data pengguna:', error);
authStore.setUser(null);
router.replace('/login');
} finally {
setLoading(false);
}
};
fetchUser();
}, [router]);

View File

@@ -1,4 +1,4 @@
// app/api/auth/_lib/session_create.ts
// src/app/api/auth/_lib/sessionCreate.ts
import { cookies } from "next/headers";
import { encrypt } from "./encrypt";
import prisma from "@/lib/prisma";
@@ -9,11 +9,13 @@ export async function sessionCreate({
exp = "30 day",
jwtSecret,
user,
invalidatePrevious = true, // 🔑 kontrol apakah sesi lama di-nonaktifkan
}: {
sessionKey: string;
exp?: string;
jwtSecret: string;
user: Record<string, unknown> & { id: string };
invalidatePrevious?: boolean; // default true untuk login, false untuk registrasi
}) {
// ✅ Validasi env vars
if (!sessionKey || sessionKey.length === 0) {
@@ -28,18 +30,19 @@ export async function sessionCreate({
throw new Error("Token generation failed");
}
// ✅ Hitung expiresAt sesuai exp
// ✅ Hitung expiresAt
let expiresAt = add(new Date(), { days: 30 });
if (exp === "7 day") expiresAt = add(new Date(), { days: 7 });
// tambahkan opsi lain jika perlu
// Sebelum create session baru, nonaktifkan session aktif sebelumnya
await prisma.userSession.updateMany({
where: { userId: user.id, active: true },
data: { active: false },
});
// 🔐 Hanya nonaktifkan sesi aktif sebelumnya jika diminta (misal: saat login ulang)
if (invalidatePrevious) {
await prisma.userSession.updateMany({
where: { userId: user.id, active: true },
data: { active: false },
});
}
// ✅ Simpan ke database
// ✅ Simpan sesi baru
await prisma.userSession.create({
data: {
token,
@@ -55,8 +58,8 @@ export async function sessionCreate({
sameSite: "lax",
path: "/",
secure: process.env.NODE_ENV === "production",
maxAge: 30 * 24 * 60 * 60, // seconds
maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik
});
return token;
}
}

View File

@@ -30,13 +30,7 @@ export async function verifySession() {
return null;
}
// ❌ Hanya tolak jika sessionInvalid = true
if (dbSession.user.sessionInvalid) {
console.log('⚠️ Session di-invalidate');
return null;
}
// ✅ Return user, meskipun isActive = false
// Don't check isActive here, let the frontend handle it
return dbSession.user;
} catch (error) {
console.warn('Session verification failed:', error);

View File

@@ -1,5 +1,4 @@
// src/app/api/auth/finalize-registration/route.ts
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
import { sessionCreate } from "../_lib/session_create";
@@ -7,7 +6,6 @@ import { sessionCreate } from "../_lib/session_create";
export async function POST(req: Request) {
try {
const { nomor, username, kodeId } = await req.json();
const cleanNomor = nomor.replace(/\D/g, "");
if (!cleanNomor || !username || !kodeId) {
@@ -17,13 +15,7 @@ export async function POST(req: Request) {
);
}
// Di awal fungsi POST
console.log("📦 Received payload:", { nomor, username, kodeId });
// Validasi OTP
const otpRecord = await prisma.kodeOtp.findUnique({
where: { id: kodeId },
});
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
return NextResponse.json(
{ success: false, message: "OTP tidak valid" },
@@ -31,7 +23,6 @@ export async function POST(req: Request) {
);
}
// Cek duplikat username
if (await prisma.user.findFirst({ where: { username } })) {
return NextResponse.json(
{ success: false, message: "Username sudah digunakan" },
@@ -39,7 +30,6 @@ export async function POST(req: Request) {
);
}
// ✅ Gunakan username dari input user
const defaultRole = await prisma.role.findFirst({
where: { name: "ADMIN DESA" },
select: { id: true },
@@ -52,23 +42,20 @@ export async function POST(req: Request) {
);
}
// ✅ Buat user dengan username yang diinput
const newUser = await prisma.user.create({
data: {
username, // ✅ Ini yang benar
username,
nomor,
roleId: defaultRole.id,
isActive: false,
},
});
// Nonaktifkan OTP
await prisma.kodeOtp.update({
where: { id: kodeId },
data: { isActive: false },
});
// ✅ BUAT SESI untuk user baru (meski isActive = false)
const token = await sessionCreate({
sessionKey: process.env.BASE_SESSION_KEY!,
jwtSecret: process.env.BASE_TOKEN_KEY!,
@@ -76,24 +63,15 @@ export async function POST(req: Request) {
user: {
id: newUser.id,
nomor: newUser.nomor,
username: newUser.username, // ✅ Pastikan sesuai
roleId: newUser.roleId,
isActive: false,
},
});
// Set cookie
const response = NextResponse.json({
success: true,
message: "Registrasi berhasil. Menunggu persetujuan admin.",
user: {
id: newUser.id,
name: newUser.username,
username: newUser.username,
roleId: newUser.roleId,
isActive: false,
},
invalidatePrevious: false,
});
// ✅ REDIRECT DARI SERVER — cookie pasti tersedia
const response = NextResponse.redirect(new URL('/waiting-room', req.url));
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
@@ -111,4 +89,4 @@ export async function POST(req: Request) {
} finally {
await prisma.$disconnect();
}
}
}

View File

@@ -0,0 +1,55 @@
import { NextResponse } from 'next/server';
import { verifySession } from '../_lib/session_verify';
import { sessionCreate } from '../_lib/session_create';
import prisma from '@/lib/prisma';
export async function POST() {
try {
const sessionUser = await verifySession();
if (!sessionUser) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 }
);
}
// Get fresh user data
const user = await prisma.user.findUnique({
where: { id: sessionUser.id },
select: {
id: true,
username: true,
roleId: true,
isActive: true,
},
});
if (!user) {
return NextResponse.json(
{ success: false, message: "User not found" },
{ status: 404 }
);
}
// Create new session with updated data
await sessionCreate({
sessionKey: process.env.BASE_SESSION_KEY!,
jwtSecret: process.env.BASE_TOKEN_KEY!,
user: {
id: user.id,
username: user.username,
roleId: user.roleId,
isActive: user.isActive,
},
invalidatePrevious: false, // Keep existing sessions
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error refreshing session:', error);
return NextResponse.json(
{ success: false, message: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -2,14 +2,22 @@
'use client';
import colors from '@/con/colors';
import { Center, Loader, Paper, Stack, Text, Title } from '@mantine/core';
import {
Button,
Center,
Loader,
Paper,
Stack,
Text,
Title,
} from '@mantine/core';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
async function fetchUser() {
const res = await fetch('/api/auth/me');
if (!res.ok) {
// Jangan throw error — biarkan handle status code
const text = await res.text();
throw new Error(`HTTP ${res.status}: ${text}`);
}
@@ -21,10 +29,14 @@ export default function WaitingRoom() {
const [user, setUser] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const [isRedirecting, setIsRedirecting] = useState(false);
const [retryCount, setRetryCount] = useState(0);
const MAX_RETRIES = 2;
useEffect(() => {
let isMounted = true;
const interval = setInterval(async () => {
let interval: ReturnType<typeof setInterval>;
const poll = async () => {
if (isRedirecting || !isMounted) return;
try {
@@ -34,63 +46,110 @@ export default function WaitingRoom() {
const currentUser = data.user;
setUser(currentUser);
// ✅ Periksa isActive dan redirect
// ✅ Update authStore
if (currentUser) {
authStore.setUser({
id: currentUser.id,
name: currentUser.name,
roleId: Number(currentUser.roleId),
menuIds: currentUser.menuIds || null,
});
}
// In the poll function
if (currentUser?.isActive === true) {
setIsRedirecting(true);
clearInterval(interval);
// ✅ roleId adalah STRING → gunakan string literal
let redirectPath = '/admin';
// Update authStore with the current user data
authStore.setUser({
id: currentUser.id,
name: currentUser.name || 'User',
roleId: Number(currentUser.roleId),
menuIds: currentUser.menuIds || null,
isActive: true
});
switch (currentUser.roleId) {
case "0": // DEVELOPER
case "1": // SUPERADMIN
case "2": // ADMIN_DESA
redirectPath = '/admin/landing-page/profil/program-inovasi';
break;
case "3": // ADMIN_KESEHATAN
redirectPath = '/admin/kesehatan/posyandu';
break;
case "4": // ADMIN_PENDIDIKAN
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
break;
// Clean up storage
localStorage.removeItem('auth_kodeId');
localStorage.removeItem('auth_nomor');
localStorage.removeItem('auth_username');
// Force a session refresh
try {
const res = await fetch('/api/auth/refresh-session', {
method: 'POST',
credentials: 'include'
});
if (res.ok) {
// Redirect based on role
let redirectPath = '/admin';
switch (String(currentUser.roleId)) {
case "0": case "1": case "2":
redirectPath = '/admin/landing-page/profil/program-inovasi';
break;
case "3":
redirectPath = '/admin/kesehatan/posyandu';
break;
case "4":
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
break;
}
window.location.href = redirectPath; // Use window.location to force full page reload
}
} catch (error) {
console.error('Error refreshing session:', error);
router.refresh(); // Fallback to client-side refresh
}
setTimeout(() => router.push(redirectPath), 500);
}
} catch (err: any) {
if (!isMounted) return;
// ❌ Hanya redirect ke /login jika benar-benar tidak ada sesi
if (err.message.includes('401')) {
setError('Sesi tidak valid');
clearInterval(interval);
router.push('/login');
if (retryCount < MAX_RETRIES) {
setRetryCount((prev) => prev + 1);
setTimeout(() => {
if (isMounted) interval = setInterval(poll, 3000);
}, 800);
} else {
setError('Sesi tidak valid. Silakan login ulang.');
clearInterval(interval);
authStore.setUser(null); // ✅ clear sesi
}
} else {
console.error('Error polling:', err);
}
}
}, 3000);
};
interval = setInterval(poll, 3000);
return () => {
isMounted = false;
clearInterval(interval);
if (interval) clearInterval(interval);
};
}, [router, isRedirecting]);
}, [router, isRedirecting, retryCount]);
// ✅ UI Error
if (error) {
return (
<Center h="100vh">
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={400}>
<Stack align="center" gap="md">
<Title order={3} c="red">Error</Title>
<Title order={3} c="red">
Sesi Tidak Valid
</Title>
<Text>{error}</Text>
<Button onClick={() => router.push('/login')}>
Login Ulang
</Button>
</Stack>
</Paper>
</Center>
);
}
// ✅ UI Redirecting
if (isRedirecting) {
return (
<Center h="100vh" bg={colors.Bg}>
@@ -109,6 +168,7 @@ export default function WaitingRoom() {
);
}
// ✅ UI Default (MENUNGGU) — INI YANG KAMU HILANGKAN!
return (
<Center h="100vh" bg={colors.Bg}>
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>

View File

@@ -6,6 +6,7 @@ export type User = {
name: string;
roleId: number;
menuIds?: string[] | null; // ✅ Pastikan pakai `string[]`
isActive?: boolean;
};
export const authStore = proxy<{