Test C 1
This commit is contained in:
@@ -31,7 +31,7 @@ export default function Validasi() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkFlow = async () => {
|
const checkFlow = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/get-flow', {
|
const res = await fetch('/api/get-flow', {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -60,7 +60,7 @@ export default function Validasi() {
|
|||||||
setKodeId(storedKodeId);
|
setKodeId(storedKodeId);
|
||||||
const loadOtpData = async () => {
|
const loadOtpData = async () => {
|
||||||
try {
|
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();
|
const result = await res.json();
|
||||||
|
|
||||||
if (res.ok && result.data?.nomor) {
|
if (res.ok && result.data?.nomor) {
|
||||||
@@ -124,7 +124,6 @@ export default function Validasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Finalize registration
|
|
||||||
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -149,7 +148,7 @@ export default function Validasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginVerification = async () => {
|
const handleLoginVerification = async () => {
|
||||||
const loginRes = await fetch('/api/auth/verify-otp-login', {
|
const loginRes = await fetch('/api/verify-otp-login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor, otp, kodeId }),
|
body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
@@ -207,7 +206,7 @@ export default function Validasi() {
|
|||||||
|
|
||||||
// Clear cookie
|
// Clear cookie
|
||||||
try {
|
try {
|
||||||
await fetch('/api/auth/clear-flow', {
|
await fetch('/api/clear-flow', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
@@ -219,7 +218,7 @@ export default function Validasi() {
|
|||||||
const handleResend = async () => {
|
const handleResend = async () => {
|
||||||
if (!nomor) return;
|
if (!nomor) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/resend', {
|
const res = await fetch('/api/resend', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor }),
|
body: JSON.stringify({ nomor }),
|
||||||
|
|||||||
144
src/app/api/[auth]/_lib/api_fetch_auth.ts
Normal file
144
src/app/api/[auth]/_lib/api_fetch_auth.ts
Normal file
@@ -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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
149
src/app/api/[auth]/finalize-registration/route.ts
Normal file
149
src/app/api/[auth]/finalize-registration/route.ts
Normal file
@@ -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<string, string[]> = {
|
||||||
|
"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();
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/app/api/[auth]/me/route.ts
Normal file
59
src/app/api/[auth]/me/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
// src/app/api/auth/finalize-registration/route.ts
|
// src/app/api/auth/finalize-registration/route.ts
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { sessionCreate } from "../_lib/session_create";
|
import { sessionCreate } from "../../[auth]/_lib/session_create";
|
||||||
|
|
||||||
|
|
||||||
// ✅ Gunakan STRING untuk roleId
|
// ✅ Gunakan STRING untuk roleId
|
||||||
const DEFAULT_MENUS_BY_ROLE: Record<string, string[]> = {
|
const DEFAULT_MENUS_BY_ROLE: Record<string, string[]> = {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default function WaitingRoom() {
|
|||||||
|
|
||||||
// Force a session refresh
|
// Force a session refresh
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/refresh-session', {
|
const res = await fetch('/api/refresh-session', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user