Fix Middleware
Fix Layout sesuai role, dan superadmin bisa menambahkan menu ke user jika diperlukan Penambahan menu di user & role : menu access
This commit is contained in:
@@ -1,7 +1,127 @@
|
||||
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// import prisma from "@/lib/prisma";
|
||||
// import { Context } from "elysia";
|
||||
|
||||
// export default async function userUpdate(context: Context) {
|
||||
// try {
|
||||
// const { id, isActive, roleId } = await context.body as {
|
||||
// id: string,
|
||||
// isActive?: boolean,
|
||||
// roleId?: string
|
||||
// };
|
||||
|
||||
// if (!id) {
|
||||
// return {
|
||||
// success: false,
|
||||
// message: "ID user wajib ada",
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Optional: cek apakah roleId valid
|
||||
// if (roleId) {
|
||||
// const cekRole = await prisma.role.findUnique({
|
||||
// where: { id: roleId }
|
||||
// });
|
||||
|
||||
// if (!cekRole) {
|
||||
// return {
|
||||
// success: false,
|
||||
// message: "Role tidak ditemukan",
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
// // ✅ 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({
|
||||
// where: { id },
|
||||
// data: {
|
||||
// ...(isActive !== undefined && { isActive }),
|
||||
// ...(roleId && { roleId }),
|
||||
// },
|
||||
// select: {
|
||||
// id: true,
|
||||
// username: true,
|
||||
// nomor: true,
|
||||
// isActive: true,
|
||||
// roleId: true,
|
||||
// updatedAt: true,
|
||||
// role: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// // ✅ 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 {
|
||||
// success: true,
|
||||
// message: isRoleChanged
|
||||
// ? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.`
|
||||
// : "User berhasil diupdate",
|
||||
// data: updatedUser,
|
||||
// roleChanged: isRoleChanged, // Info untuk frontend
|
||||
// oldRoleId: oldRoleId,
|
||||
// newRoleId: roleId,
|
||||
// };
|
||||
|
||||
// } catch (e: any) {
|
||||
// console.error("❌ Error update user:", e);
|
||||
// return {
|
||||
// success: false,
|
||||
// message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
|
||||
// API update user (Elysia atau Next.js API Route)
|
||||
export default async function userUpdate(context: Context) {
|
||||
try {
|
||||
const { id, isActive, roleId } = await context.body as {
|
||||
@@ -17,12 +137,9 @@ export default async function userUpdate(context: Context) {
|
||||
};
|
||||
}
|
||||
|
||||
// Optional: cek apakah roleId valid
|
||||
// Cek apakah roleId valid
|
||||
if (roleId) {
|
||||
const cekRole = await prisma.role.findUnique({
|
||||
where: { id: roleId }
|
||||
});
|
||||
|
||||
const cekRole = await prisma.role.findUnique({ where: { id: roleId } });
|
||||
if (!cekRole) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -31,32 +148,24 @@ export default async function userUpdate(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CEK: Apakah roleId berubah?
|
||||
// Deteksi perubahan role
|
||||
let isRoleChanged = false;
|
||||
let oldRoleId: string | null = null;
|
||||
|
||||
if (roleId) {
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
roleId: true,
|
||||
username: true,
|
||||
}
|
||||
select: { roleId: true }
|
||||
});
|
||||
|
||||
if (currentUser && currentUser.roleId !== roleId) {
|
||||
isRoleChanged = true;
|
||||
oldRoleId = currentUser.roleId;
|
||||
console.log(`🔄 Role berubah untuk ${currentUser.username}: ${oldRoleId} → ${roleId}`);
|
||||
}
|
||||
isRoleChanged = currentUser?.roleId !== roleId;
|
||||
}
|
||||
|
||||
// Update user
|
||||
// ✅ UPDATE USER + INVALIDATE SESSION
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(isActive !== undefined && { isActive }),
|
||||
...(roleId && { roleId }),
|
||||
// Force logout: set sessionInvalid = true
|
||||
...(isRoleChanged && { sessionInvalid: true }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -64,48 +173,31 @@ export default async function userUpdate(context: Context) {
|
||||
nomor: true,
|
||||
isActive: true,
|
||||
roleId: true,
|
||||
updatedAt: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
role: { select: { name: true } }
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ FORCE LOGOUT: Hapus UserSession jika role berubah
|
||||
// ✅ Reset sessionInvalid setelah 5 detik (opsional)
|
||||
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`);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { sessionInvalid: false }
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Gagal reset sessionInvalid:', e);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ✅ Response dengan info tambahan
|
||||
return {
|
||||
success: true,
|
||||
message: isRoleChanged
|
||||
? `User berhasil diupdate. ${updatedUser.username} akan logout otomatis.`
|
||||
: "User berhasil diupdate",
|
||||
data: updatedUser,
|
||||
roleChanged: isRoleChanged, // Info untuk frontend
|
||||
oldRoleId: oldRoleId,
|
||||
newRoleId: roleId,
|
||||
};
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("❌ Error update user:", e);
|
||||
return {
|
||||
|
||||
65
src/app/api/admin/user-menu-access/route.ts
Normal file
65
src/app/api/admin/user-menu-access/route.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// src/app/api/admin/user-menu-access/route.ts
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// ❌ HAPUS { params } karena tidak dipakai
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'User ID diperlukan' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const menuAccess = await prisma.userMenuAccess.findMany({
|
||||
where: { userId },
|
||||
select: { menuId: true },
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
menuIds: menuAccess.map(m => m.menuId),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('GET User Menu Access Error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Gagal memuat menu akses' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// POST tetap sama (tanpa perubahan)
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { userId, menuIds } = await request.json()
|
||||
|
||||
if (!userId || !Array.isArray(menuIds)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Data tidak valid' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await prisma.userMenuAccess.deleteMany({ where: { userId } })
|
||||
|
||||
if (menuIds.length > 0) {
|
||||
await prisma.userMenuAccess.createMany({
|
||||
data: menuIds.map((menuId: string) => ({ userId, menuId })),
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('POST User Menu Access Error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Gagal menyimpan menu akses' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -117,4 +117,44 @@ export const apiFetchVerifyOtp = async ({
|
||||
...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' };
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,136 @@
|
||||
// app/api/auth/_lib/session_verify.ts
|
||||
// // 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;
|
||||
// }
|
||||
// }
|
||||
|
||||
// src/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> {
|
||||
export async function verifySession() {
|
||||
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');
|
||||
|
||||
if (!sessionKey || !jwtSecret) {
|
||||
throw new Error('Environment variables tidak lengkap');
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get(sessionKey)?.value;
|
||||
const token = (await cookies()).get(sessionKey)?.value;
|
||||
if (!token) return null;
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 1: Decrypt JWT
|
||||
// Decrypt JWT
|
||||
const jwtUser = await decrypt({ token, jwtSecret });
|
||||
|
||||
if (!jwtUser || !jwtUser.id) {
|
||||
console.log('⚠️ JWT decrypt failed atau tidak ada user ID');
|
||||
if (!jwtUser || !jwtUser.id) return null;
|
||||
|
||||
// ✅ Cek apakah session di-invalidate
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: jwtUser.id as string },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nomor: true,
|
||||
roleId: true,
|
||||
isActive: true,
|
||||
sessionInvalid: true, // ← Tambahkan field ini
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || user.sessionInvalid) {
|
||||
console.log('⚠️ Session tidak valid (force logout)');
|
||||
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;
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.warn('❌ Session verification failed:', error);
|
||||
console.warn('Session verification failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
// app/api/auth/me/route.ts
|
||||
// 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 {
|
||||
// ✅ Verify session (hybrid: JWT + Database)
|
||||
const user = await verifySession();
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Session tidak valid",
|
||||
user: null
|
||||
},
|
||||
{ success: false, message: "Session tidak valid", user: null },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Data user sudah fresh dari database (via verifySession)
|
||||
// ✅ Ambil menu akses kustom
|
||||
const menuAccess = await prisma.userMenuAccess.findMany({
|
||||
where: { userId: user.id },
|
||||
select: { menuId: true },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
@@ -28,17 +28,13 @@ export async function GET() {
|
||||
nomor: user.nomor,
|
||||
roleId: user.roleId,
|
||||
isActive: user.isActive,
|
||||
menuIds: menuAccess.map(m => m.menuId), // ✅ tambahkan ini
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Error in /api/auth/me:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Terjadi kesalahan",
|
||||
user: null
|
||||
},
|
||||
{ success: false, message: "Terjadi kesalahan", user: null },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user