diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx index e6fe0a4b..958c8605 100644 --- a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx +++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx @@ -31,7 +31,7 @@ export default function Validasi() { useEffect(() => { const checkFlow = async () => { try { - const res = await fetch('/api/auth/get-flow', { + const res = await fetch('/api/get-flow', { credentials: 'include' }); const data = await res.json(); @@ -60,7 +60,7 @@ export default function Validasi() { setKodeId(storedKodeId); const loadOtpData = async () => { try { - const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`); + const res = await fetch(`/api/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`); const result = await res.json(); if (res.ok && result.data?.nomor) { @@ -124,7 +124,6 @@ export default function Validasi() { return; } - // ✅ Finalize registration const finalizeRes = await fetch('/api/auth/finalize-registration', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -149,7 +148,7 @@ export default function Validasi() { }; const handleLoginVerification = async () => { - const loginRes = await fetch('/api/auth/verify-otp-login', { + const loginRes = await fetch('/api/verify-otp-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nomor, otp, kodeId }), @@ -207,7 +206,7 @@ export default function Validasi() { // Clear cookie try { - await fetch('/api/auth/clear-flow', { + await fetch('/api/clear-flow', { method: 'POST', credentials: 'include' }); @@ -219,7 +218,7 @@ export default function Validasi() { const handleResend = async () => { if (!nomor) return; try { - const res = await fetch('/api/auth/resend', { + const res = await fetch('/api/resend', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nomor }), diff --git a/src/app/api/[auth]/_lib/api_fetch_auth.ts b/src/app/api/[auth]/_lib/api_fetch_auth.ts new file mode 100644 index 00000000..44c4fddb --- /dev/null +++ b/src/app/api/[auth]/_lib/api_fetch_auth.ts @@ -0,0 +1,144 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// app/api/_lib/api_fetch_auth.ts + +// app/api/_lib/api_fetch_auth.ts + +export const apiFetchLogin = async ({ nomor }: { nomor: string }) => { + if (!nomor || nomor.replace(/\D/g, '').length < 10) { + throw new Error('Nomor tidak valid'); + } + + const cleanPhone = nomor.replace(/\D/g, ''); + + const response = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ nomor: cleanPhone }), + credentials: 'include' + }); + + // Pastikan respons bisa di-parse sebagai JSON + let data; + try { + data = await response.json(); + } catch (e) { + console.error("Non-JSON response from /api/login:", await response.text()); + throw new Error('Respons server tidak valid'); + } + + if (!response.ok) { + throw new Error(data.message || 'Gagal memproses login'); + } + + // Validasi minimal respons + if (typeof data.success !== 'boolean' || typeof data.isRegistered !== 'boolean') { + throw new Error('Respons tidak sesuai format'); + } + + if (data.success) { + if (data.isRegistered && !data.kodeId) { + throw new Error('Kode verifikasi tidak ditemukan untuk user terdaftar'); + } + return data; // { success, isRegistered, kodeId? } + } else { + throw new Error(data.message || 'Login gagal'); + } +}; + +export const apiFetchRegister = async ({ + username, + nomor, +}: { + username: string; + nomor: string; +}) => { + const cleanPhone = nomor.replace(/\D/g, ''); + if (cleanPhone.length < 10) throw new Error('Nomor tidak valid'); + + const response = await fetch("/api/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }), + credentials: 'include', + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.message || 'Gagal mengirim OTP'); + + return data; +}; + +export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => { + if (!kodeId) { + throw new Error('Kode ID tidak valid'); + } + + const response = await fetch("/api/otp-data", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ kodeId }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Gagal memuat data OTP'); + } + + return data; +}; + +// Ganti endpoint ke verify-otp-login +export const apiFetchVerifyOtp = async ({ nomor, otp, kodeId }: { nomor: string; otp: string; kodeId: string }) => { + const response = await fetch('/api/verify-otp-login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nomor, otp, kodeId }), + }); + const data = await response.json(); + return { + success: response.ok, + ...data, + status: response.status, + }; +}; + +// Di dalam api_fetch_auth.ts + +export async function apiFetchUserMenuAccess(userId: string): Promise<{ + success: boolean; + menuIds?: string[]; + message?: string; +}> { + try { + const res = await fetch(`/api/admin/user-menu-access/${userId}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + + const data = await res.json(); + return data; + } catch (error) { + console.error('API Fetch User Menu Access Error:', error); + return { success: false, message: 'Gagal memuat menu akses' }; + } +} + +export async function apiUpdateUserMenuAccess( + userId: string, + menuIds: string[] +): Promise<{ success: boolean; message?: string }> { + try { + const res = await fetch('/api/admin/user-menu-access', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId, menuIds }), + }); + + const data = await res.json(); + return data; + } catch (error) { + console.error('API Update User Menu Access Error:', error); + return { success: false, message: 'Gagal menyimpan menu akses' }; + } +} \ No newline at end of file diff --git a/src/app/api/auth/_lib/decrypt.ts b/src/app/api/[auth]/_lib/decrypt.ts similarity index 100% rename from src/app/api/auth/_lib/decrypt.ts rename to src/app/api/[auth]/_lib/decrypt.ts diff --git a/src/app/api/auth/_lib/encrypt.ts b/src/app/api/[auth]/_lib/encrypt.ts similarity index 100% rename from src/app/api/auth/_lib/encrypt.ts rename to src/app/api/[auth]/_lib/encrypt.ts diff --git a/src/app/api/auth/_lib/get_KodeOtp_By_Id.ts b/src/app/api/[auth]/_lib/get_KodeOtp_By_Id.ts similarity index 100% rename from src/app/api/auth/_lib/get_KodeOtp_By_Id.ts rename to src/app/api/[auth]/_lib/get_KodeOtp_By_Id.ts diff --git a/src/app/api/auth/_lib/randomOTP.ts b/src/app/api/[auth]/_lib/randomOTP.ts similarity index 100% rename from src/app/api/auth/_lib/randomOTP.ts rename to src/app/api/[auth]/_lib/randomOTP.ts diff --git a/src/app/api/auth/_lib/session_create.ts b/src/app/api/[auth]/_lib/session_create.ts similarity index 100% rename from src/app/api/auth/_lib/session_create.ts rename to src/app/api/[auth]/_lib/session_create.ts diff --git a/src/app/api/auth/_lib/session_delete.ts b/src/app/api/[auth]/_lib/session_delete.ts similarity index 100% rename from src/app/api/auth/_lib/session_delete.ts rename to src/app/api/[auth]/_lib/session_delete.ts diff --git a/src/app/api/auth/_lib/session_verify.ts b/src/app/api/[auth]/_lib/session_verify.ts similarity index 100% rename from src/app/api/auth/_lib/session_verify.ts rename to src/app/api/[auth]/_lib/session_verify.ts diff --git a/src/app/api/auth/clear-flow/route.ts b/src/app/api/[auth]/clear-flow/route.ts similarity index 100% rename from src/app/api/auth/clear-flow/route.ts rename to src/app/api/[auth]/clear-flow/route.ts diff --git a/src/app/api/[auth]/finalize-registration/route.ts b/src/app/api/[auth]/finalize-registration/route.ts new file mode 100644 index 00000000..40faa181 --- /dev/null +++ b/src/app/api/[auth]/finalize-registration/route.ts @@ -0,0 +1,149 @@ +// src/app/api/auth/finalize-registration/route.ts +import prisma from "@/lib/prisma"; +import { NextResponse } from "next/server"; +import { sessionCreate } from "../_lib/session_create"; + +// ✅ Gunakan STRING untuk roleId +const DEFAULT_MENUS_BY_ROLE: Record = { + "0": [ + "Landing Page", "PPID", "Desa", "Kesehatan", "Keamanan", + "Ekonomi", "Inovasi", "Lingkungan", "Pendidikan", "User & Role" + ], + "1": [ + "Landing Page", "PPID", "Desa", "Keamanan", + "Ekonomi", "Inovasi", "Lingkungan", "User & Role" + ], + "2": ["Landing Page", "Desa", "Ekonomi", "Inovasi", "Lingkungan"], + "3": ["Kesehatan"], + "4": ["Pendidikan"], +}; + +export async function POST(req: Request) { + try { + const { nomor, username, kodeId } = await req.json(); + const cleanNomor = nomor.replace(/\D/g, ""); + + if (!cleanNomor || !username || !kodeId) { + return NextResponse.json( + { success: false, message: "Data tidak lengkap" }, + { status: 400 } + ); + } + + // Verify OTP + const otpRecord = await prisma.kodeOtp.findUnique({ + where: { id: kodeId }, + select: { nomor: true, isActive: true } + }); + + if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) { + return NextResponse.json( + { success: false, message: "OTP tidak valid" }, + { status: 400 } + ); + } + + // Check duplicate username + if (await prisma.user.findFirst({ where: { username } })) { + return NextResponse.json( + { success: false, message: "Username sudah digunakan" }, + { status: 409 } + ); + } + + // Check duplicate nomor + if (await prisma.user.findUnique({ where: { nomor: cleanNomor } })) { + return NextResponse.json( + { success: false, message: "Nomor sudah terdaftar" }, + { status: 409 } + ); + } + + // 🔥 Tentukan roleId sebagai STRING + const targetRoleId = "2"; // ✅ Default ADMIN_DESA (roleId "2") + + // Validasi role exists + const roleExists = await prisma.role.findUnique({ + where: { id: targetRoleId }, + select: { id: true } + }); + + if (!roleExists) { + return NextResponse.json( + { success: false, message: "Role tidak valid" }, + { status: 400 } + ); + } + + // ✅ Create user (inactive, waiting approval) + const newUser = await prisma.user.create({ + data: { + username, + nomor: cleanNomor, + roleId: targetRoleId, + isActive: false, // Waiting for admin approval + }, + }); + + // ✅ Berikan akses menu default based on role + const menuIds = DEFAULT_MENUS_BY_ROLE[targetRoleId] || []; + if (menuIds.length > 0) { + await prisma.userMenuAccess.createMany({ + data: menuIds.map(menuId => ({ + userId: newUser.id, + menuId, + })), + skipDuplicates: true, // ✅ Avoid duplicate errors + }); + } + + // ✅ Mark OTP as used + await prisma.kodeOtp.update({ + where: { id: kodeId }, + data: { isActive: false }, + }); + + // ✅ Create session token + const token = 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 belum aktif + }, + invalidatePrevious: false, + }); + + // ✅ PENTING: Return JSON response (bukan redirect) + const response = NextResponse.json({ + success: true, + message: "Registrasi berhasil. Menunggu persetujuan admin.", + userId: newUser.id, + }); + + // ✅ Set session cookie + const cookieName = process.env.BASE_SESSION_KEY || 'session'; + response.cookies.set(cookieName, token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: 'lax', + path: "/", + maxAge: 30 * 24 * 60 * 60, // 30 days + }); + + return response; + + } catch (error) { + console.error("❌ Finalize Registration Error:", error); + return NextResponse.json( + { success: false, message: "Registrasi gagal. Silakan coba lagi." }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/get-flow/route.ts b/src/app/api/[auth]/get-flow/route.ts similarity index 100% rename from src/app/api/auth/get-flow/route.ts rename to src/app/api/[auth]/get-flow/route.ts diff --git a/src/app/api/auth/login/route.ts b/src/app/api/[auth]/login/route.ts similarity index 100% rename from src/app/api/auth/login/route.ts rename to src/app/api/[auth]/login/route.ts diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/[auth]/logout/route.ts similarity index 100% rename from src/app/api/auth/logout/route.ts rename to src/app/api/[auth]/logout/route.ts diff --git a/src/app/api/[auth]/me/route.ts b/src/app/api/[auth]/me/route.ts new file mode 100644 index 00000000..d497926b --- /dev/null +++ b/src/app/api/[auth]/me/route.ts @@ -0,0 +1,59 @@ +// src/app/api/auth/me/route.ts +import { NextResponse } from 'next/server'; +import { verifySession } from '../_lib/session_verify'; +import prisma from '@/lib/prisma'; + +export async function GET() { + try { + const sessionUser = await verifySession(); + if (!sessionUser) { + return NextResponse.json( + { success: false, message: "Unauthorized", user: null }, + { status: 401 } + ); + } + + const [dbUser, menuAccess] = await Promise.all([ + prisma.user.findUnique({ + where: { id: sessionUser.id }, + select: { + id: true, + username: true, + nomor: true, + roleId: true, // STRING! + isActive: true, // BOOLEAN! + }, + }), + prisma.userMenuAccess.findMany({ + where: { userId: sessionUser.id }, + select: { menuId: true }, + }), + ]); + + if (!dbUser) { + return NextResponse.json( + { success: false, message: "User not found", user: null }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + user: { + id: dbUser.id, + name: dbUser.username, + username: dbUser.username, + nomor: dbUser.nomor, + roleId: dbUser.roleId, // STRING! + isActive: dbUser.isActive, // BOOLEAN! + menuIds: menuAccess.map(m => m.menuId), + }, + }); + } catch (error) { + console.error("❌ Error in /api/me:", error); + return NextResponse.json( + { success: false, message: "Internal server error", user: null }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/auth/otp-data/route.ts b/src/app/api/[auth]/otp-data/route.ts similarity index 100% rename from src/app/api/auth/otp-data/route.ts rename to src/app/api/[auth]/otp-data/route.ts diff --git a/src/app/api/auth/refresh-session/route.ts b/src/app/api/[auth]/refresh-session/route.ts similarity index 100% rename from src/app/api/auth/refresh-session/route.ts rename to src/app/api/[auth]/refresh-session/route.ts diff --git a/src/app/api/auth/register/route.ts b/src/app/api/[auth]/register/route.ts similarity index 100% rename from src/app/api/auth/register/route.ts rename to src/app/api/[auth]/register/route.ts diff --git a/src/app/api/auth/resend/route.ts b/src/app/api/[auth]/resend/route.ts similarity index 100% rename from src/app/api/auth/resend/route.ts rename to src/app/api/[auth]/resend/route.ts diff --git a/src/app/api/auth/send-otp-register/route.ts b/src/app/api/[auth]/send-otp-register/route.ts similarity index 100% rename from src/app/api/auth/send-otp-register/route.ts rename to src/app/api/[auth]/send-otp-register/route.ts diff --git a/src/app/api/auth/set-flow/route.ts b/src/app/api/[auth]/set-flow/route.ts similarity index 100% rename from src/app/api/auth/set-flow/route.ts rename to src/app/api/[auth]/set-flow/route.ts diff --git a/src/app/api/auth/validasi/route.ts b/src/app/api/[auth]/validasi/route.ts similarity index 100% rename from src/app/api/auth/validasi/route.ts rename to src/app/api/[auth]/validasi/route.ts diff --git a/src/app/api/auth/verify-otp-login/route.ts b/src/app/api/[auth]/verify-otp-login/route.ts similarity index 100% rename from src/app/api/auth/verify-otp-login/route.ts rename to src/app/api/[auth]/verify-otp-login/route.ts diff --git a/src/app/api/auth/verify-otp-register/route.ts b/src/app/api/[auth]/verify-otp-register/route.ts similarity index 100% rename from src/app/api/auth/verify-otp-register/route.ts rename to src/app/api/[auth]/verify-otp-register/route.ts diff --git a/src/app/api/auth/finalize-registration/route.ts b/src/app/api/auth/finalize-registration/route.ts index 40faa181..007ece3c 100644 --- a/src/app/api/auth/finalize-registration/route.ts +++ b/src/app/api/auth/finalize-registration/route.ts @@ -1,7 +1,8 @@ // src/app/api/auth/finalize-registration/route.ts import prisma from "@/lib/prisma"; import { NextResponse } from "next/server"; -import { sessionCreate } from "../_lib/session_create"; +import { sessionCreate } from "../../[auth]/_lib/session_create"; + // ✅ Gunakan STRING untuk roleId const DEFAULT_MENUS_BY_ROLE: Record = { diff --git a/src/app/waiting-room/page.tsx b/src/app/waiting-room/page.tsx index c725dd5b..f04fca06 100644 --- a/src/app/waiting-room/page.tsx +++ b/src/app/waiting-room/page.tsx @@ -80,7 +80,7 @@ export default function WaitingRoom() { // Force a session refresh try { - const res = await fetch('/api/auth/refresh-session', { + const res = await fetch('/api/refresh-session', { method: 'POST', credentials: 'include' });