Compare commits
23 Commits
nico/12-ja
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
| 26ae1dfb1b | |||
| 234a02e849 | |||
| 56df11419d | |||
| 8fed1dfda4 | |||
| 091c33a73c | |||
| ff9c3668fc | |||
| e2e8b47868 | |||
| a389e5ee32 | |||
| d7c694d237 | |||
| 4d3b2dd3f3 | |||
| 662dfc939e | |||
| fee1a6dfb2 | |||
| afe8c70cef | |||
| fb010bd05a | |||
| cfe60ed8fe | |||
| dc56a329dc | |||
| a9195d30bd | |||
| 569b0d408b | |||
| 40985f961a | |||
| 22424ef53e | |||
| 14b49334ac | |||
| 4a6829c502 | |||
| 60b035749d |
@@ -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 }),
|
||||
|
||||
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
|
||||
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<string, string[]> = {
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user