From 2fb3666e579090d08431603847cde9e3fc87427a Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 26 Nov 2025 10:14:05 +0800 Subject: [PATCH] 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 --- .../(dashboard)/auth/validasi-admin/page.tsx | 90 ++++++-------- src/app/admin/layout.tsx | 65 +++++----- src/app/api/auth/_lib/session_create.ts | 25 ++-- src/app/api/auth/_lib/session_verify.ts | 8 +- .../api/auth/finalize-registration/route.ts | 36 ++---- src/app/api/auth/refresh-session/route.ts | 55 +++++++++ src/app/waiting-room/page.tsx | 116 +++++++++++++----- src/store/authStore.ts | 1 + 8 files changed, 239 insertions(+), 157 deletions(-) create mode 100644 src/app/api/auth/refresh-session/route.ts diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx index 403f3844..0ed24cd3 100644 --- a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx +++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx @@ -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 () => { diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 3b1cfb99..1d25f98c 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -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]); diff --git a/src/app/api/auth/_lib/session_create.ts b/src/app/api/auth/_lib/session_create.ts index 8217693f..121b6b62 100644 --- a/src/app/api/auth/_lib/session_create.ts +++ b/src/app/api/auth/_lib/session_create.ts @@ -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 & { 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; -} +} \ No newline at end of file diff --git a/src/app/api/auth/_lib/session_verify.ts b/src/app/api/auth/_lib/session_verify.ts index 8b799a34..2a934a19 100644 --- a/src/app/api/auth/_lib/session_verify.ts +++ b/src/app/api/auth/_lib/session_verify.ts @@ -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); diff --git a/src/app/api/auth/finalize-registration/route.ts b/src/app/api/auth/finalize-registration/route.ts index 51890a8a..fe50ae08 100644 --- a/src/app/api/auth/finalize-registration/route.ts +++ b/src/app/api/auth/finalize-registration/route.ts @@ -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(); } -} +} \ No newline at end of file diff --git a/src/app/api/auth/refresh-session/route.ts b/src/app/api/auth/refresh-session/route.ts new file mode 100644 index 00000000..16baf54c --- /dev/null +++ b/src/app/api/auth/refresh-session/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/src/app/waiting-room/page.tsx b/src/app/waiting-room/page.tsx index fe8841f6..a6ec5c74 100644 --- a/src/app/waiting-room/page.tsx +++ b/src/app/waiting-room/page.tsx @@ -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(null); const [error, setError] = useState(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; + + 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 (
- Error + + Sesi Tidak Valid + {error} +
); } + // ✅ UI Redirecting if (isRedirecting) { return (
@@ -109,6 +168,7 @@ export default function WaitingRoom() { ); } + // ✅ UI Default (MENUNGGU) — INI YANG KAMU HILANGKAN! return (
diff --git a/src/store/authStore.ts b/src/store/authStore.ts index b819002d..f597367e 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -6,6 +6,7 @@ export type User = { name: string; roleId: number; menuIds?: string[] | null; // ✅ Pastikan pakai `string[]` + isActive?: boolean; }; export const authStore = proxy<{