Tambah cookies di bagian verifikasi, agar kedeteksi user sudah regis apa belom

This commit is contained in:
2025-11-27 14:46:49 +08:00
parent c64a2e5457
commit 2cecec733e
8 changed files with 209 additions and 62 deletions

View File

@@ -15,7 +15,9 @@ function Login() {
// Login.tsx // Login.tsx
async function onLogin() { async function onLogin() {
const cleanPhone = phone.replace(/\D/g, ''); const cleanPhone = phone.replace(/\D/g, '');
console.log(cleanPhone);
if (cleanPhone.length < 10) { if (cleanPhone.length < 10) {
toast.error('Nomor telepon tidak valid'); toast.error('Nomor telepon tidak valid');
return; return;
@@ -25,6 +27,8 @@ function Login() {
setLoading(true); setLoading(true);
const response = await apiFetchLogin({ nomor: cleanPhone }); const response = await apiFetchLogin({ nomor: cleanPhone });
console.log(response);
if (!response.success) { if (!response.success) {
toast.error(response.message || 'Gagal memproses login'); toast.error(response.message || 'Gagal memproses login');
return; return;
@@ -32,11 +36,12 @@ function Login() {
// Simpan nomor untuk register // Simpan nomor untuk register
localStorage.setItem('auth_nomor', cleanPhone); localStorage.setItem('auth_nomor', cleanPhone);
if (response.isRegistered) { if (response.isRegistered) {
// ✅ User lama: simpan kodeId & ke validasi // ✅ User lama: simpan kodeId
localStorage.setItem('auth_kodeId', response.kodeId); localStorage.setItem('auth_kodeId', response.kodeId);
router.push('/validasi');
// ✅ Cookie sudah di-set oleh API, langsung redirect
router.push('/validasi'); // Clean URL
} else { } else {
// ❌ User baru: langsung ke registrasi (tanpa kodeId) // ❌ User baru: langsung ke registrasi (tanpa kodeId)
router.push('/registrasi'); router.push('/registrasi');

View File

@@ -19,17 +19,34 @@ import { authStore } from '@/store/authStore';
export default function Validasi() { export default function Validasi() {
const router = useRouter(); const router = useRouter();
const [nomor, setNomor] = useState<string | null>(null); const [nomor, setNomor] = useState<string | null>(null);
const [otp, setOtp] = useState(''); const [otp, setOtp] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [kodeId, setKodeId] = useState<string | null>(null); const [kodeId, setKodeId] = useState<string | null>(null);
const [isRegistrationFlow, setIsRegistrationFlow] = useState(false); // Tambahkan flag const [isRegistrationFlow, setIsRegistrationFlow] = useState(false);
// Cek apakah ini alur registrasi // ✅ Deteksi flow dari cookie via API
useEffect(() => { useEffect(() => {
const storedUsername = localStorage.getItem('auth_username'); const checkFlow = async () => {
setIsRegistrationFlow(!!storedUsername); try {
const res = await fetch('/api/auth/get-flow', {
credentials: 'include'
});
const data = await res.json();
if (data.success) {
setIsRegistrationFlow(data.flow === 'register');
console.log('🔍 Flow detected from cookie:', data.flow);
}
} catch (error) {
console.error('❌ Error getting flow:', error);
setIsRegistrationFlow(false);
}
};
checkFlow();
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -68,10 +85,8 @@ export default function Validasi() {
setLoading(true); setLoading(true);
try { try {
if (isRegistrationFlow) { if (isRegistrationFlow) {
// 🔑 Alur REGISTRASI
await handleRegistrationVerification(); await handleRegistrationVerification();
} else { } else {
// 🔑 Alur LOGIN
await handleLoginVerification(); await handleLoginVerification();
} }
} catch (error) { } catch (error) {
@@ -82,55 +97,55 @@ export default function Validasi() {
} }
}; };
const handleRegistrationVerification = async () => { const handleRegistrationVerification = async () => {
const username = localStorage.getItem('auth_username'); const username = localStorage.getItem('auth_username');
if (!username) { if (!username) {
toast.error('Data registrasi tidak ditemukan.'); toast.error('Data registrasi tidak ditemukan.');
return; return;
} }
const cleanNomor = nomor?.replace(/\D/g, '') ?? ''; const cleanNomor = nomor?.replace(/\D/g, '') ?? '';
if (cleanNomor.length < 10 || username.trim().length < 5) { if (cleanNomor.length < 10 || username.trim().length < 5) {
toast.error('Data tidak valid'); toast.error('Data tidak valid');
return; return;
} }
// Verifikasi OTP dulu const verifyRes = await fetch('/api/auth/verify-otp-register', {
const verifyRes = await fetch('/api/auth/verify-otp-register', { method: 'POST',
method: 'POST', headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }), });
});
const verifyData = await verifyRes.json(); const verifyData = await verifyRes.json();
if (!verifyRes.ok) { if (!verifyRes.ok) {
toast.error(verifyData.message || 'Verifikasi OTP gagal'); toast.error(verifyData.message || 'Verifikasi OTP gagal');
return; return;
} }
// ✅ Kirim ke finalize-registration → akan redirect ke /waiting-room 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' }, body: JSON.stringify({ nomor, username, kodeId }),
body: JSON.stringify({ nomor, username, kodeId }), credentials: 'include'
credentials: 'include' });
});
if (finalizeRes.redirected) {
// ✅ Redirect otomatis oleh server
window.location.href = finalizeRes.url;
} else {
const data = await finalizeRes.json(); const data = await finalizeRes.json();
toast.error(data.message || 'Registrasi gagal');
} if (data.success || finalizeRes.redirected) {
}; // ✅ Cleanup setelah registrasi sukses
await cleanupStorage();
window.location.href = '/waiting-room';
} else {
toast.error(data.message || 'Registrasi gagal');
}
};
// ✅ Verifikasi OTP untuk LOGIN
const handleLoginVerification = async () => { const handleLoginVerification = async () => {
const loginRes = await fetch('/api/auth/verify-otp-login', { const loginRes = await fetch('/api/auth/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 }),
credentials: 'include'
}); });
const loginData = await loginRes.json(); const loginData = await loginRes.json();
@@ -148,7 +163,8 @@ const handleRegistrationVerification = async () => {
roleId: Number(roleId), roleId: Number(roleId),
}); });
cleanupStorage(); // ✅ Cleanup setelah login sukses
await cleanupStorage();
if (!isActive) { if (!isActive) {
window.location.href = '/waiting-room'; window.location.href = '/waiting-room';
@@ -161,23 +177,35 @@ const handleRegistrationVerification = async () => {
const getRedirectPath = (roleId: number): string => { const getRedirectPath = (roleId: number): string => {
switch (roleId) { switch (roleId) {
case 0: // DEVELOPER case 0:
case 1: // SUPERADMIN case 1:
case 2: // ADMIN_DESA case 2:
return '/admin/landing-page/profil/program-inovasi'; return '/admin/landing-page/profil/program-inovasi';
case 3: // ADMIN_KESEHATAN case 3:
return '/admin/kesehatan/posyandu'; return '/admin/kesehatan/posyandu';
case 4: // ADMIN_PENDIDIKAN case 4:
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan'; return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
default: default:
return '/admin'; return '/admin';
} }
}; };
const cleanupStorage = () => { // ✅ CLEANUP FUNCTION - Hapus localStorage + Cookie
const cleanupStorage = async () => {
// Clear localStorage
localStorage.removeItem('auth_kodeId'); localStorage.removeItem('auth_kodeId');
localStorage.removeItem('auth_nomor'); localStorage.removeItem('auth_nomor');
localStorage.removeItem('auth_username'); localStorage.removeItem('auth_username');
// Clear cookie
try {
await fetch('/api/auth/clear-flow', {
method: 'POST',
credentials: 'include'
});
} catch (error) {
console.error('Error clearing flow cookie:', error);
}
}; };
const handleResend = async () => { const handleResend = async () => {

View File

@@ -14,6 +14,7 @@ export const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nomor: cleanPhone }), body: JSON.stringify({ nomor: cleanPhone }),
credentials: 'include'
}); });
// Pastikan respons bisa di-parse sebagai JSON // Pastikan respons bisa di-parse sebagai JSON
@@ -58,6 +59,7 @@ export const apiFetchRegister = async ({
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }), body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }),
credentials: 'include',
}); });
const data = await response.json(); const data = await response.json();

View File

@@ -0,0 +1,19 @@
// app/api/auth/clear-flow/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function POST() {
try {
// ✅ Next.js 15 syntax
const cookieStore = await cookies();
cookieStore.delete('auth_flow');
return NextResponse.json({ success: true });
} catch (error) {
console.error('❌ Error clearing flow cookie:', error);
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,22 @@
// app/api/auth/get-flow/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function GET() {
try {
// ✅ Next.js 15 syntax
const cookieStore = await cookies();
const flow = cookieStore.get('auth_flow')?.value || 'login';
return NextResponse.json({
success: true,
flow
});
} catch (error) {
console.error('❌ Error getting flow cookie:', error);
return NextResponse.json(
{ success: false, flow: 'login' },
{ status: 500 }
);
}
}

View File

@@ -2,6 +2,7 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { randomOTP } from "../_lib/randomOTP"; import { randomOTP } from "../_lib/randomOTP";
import { cookies } from "next/headers";
export async function POST(req: Request) { export async function POST(req: Request) {
if (req.method !== "POST") { if (req.method !== "POST") {
@@ -29,19 +30,37 @@ export async function POST(req: Request) {
const isRegistered = !!existingUser; const isRegistered = !!existingUser;
if (isRegistered) { if (isRegistered) {
// ✅ User terdaftar → kirim OTP
const codeOtp = randomOTP(); const codeOtp = randomOTP();
const otpNumber = Number(codeOtp); const otpNumber = Number(codeOtp);
const waMessage = `Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`; // ✅ PERBAIKAN: Gunakan format pesan yang lebih sederhana
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`; // Hapus karakter khusus yang bisa bikin masalah
const waMessage = `Website Desa Darmasaba\nKode verifikasi Anda ${codeOtp}`;
// ✅ OPSI 1: Tanpa encoding (coba dulu ini)
const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${waMessage}`;
// ✅ OPSI 2: Dengan encoding (kalau opsi 1 gagal)
// const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodeURIComponent(waMessage)}`;
// ✅ OPSI 3: Encoding manual untuk URL-safe (alternatif terakhir)
// const encodedMessage = waMessage.replace(/\n/g, '%0A').replace(/ /g, '%20');
// const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodedMessage}`;
console.log("🔍 Debug WA URL:", waUrl); // Untuk debugging
const res = await fetch(waUrl); const res = await fetch(waUrl);
const sendWa = await res.json(); const sendWa = await res.json();
console.log("📱 WA Response:", sendWa); // Debug response
if (sendWa.status !== "success") { if (sendWa.status !== "success") {
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Gagal mengirim OTP via WhatsApp" }, {
success: false,
message: "Gagal mengirim OTP via WhatsApp",
debug: sendWa // Tampilkan error detail
},
{ status: 400 } { status: 400 }
); );
} }
@@ -50,6 +69,15 @@ export async function POST(req: Request) {
data: { nomor, otp: otpNumber, isActive: true }, data: { nomor, otp: otpNumber, isActive: true },
}); });
const cookieStore = await cookies();
cookieStore.set('auth_flow', 'login', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 5, // 5 menit
path: '/'
});
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: "Kode verifikasi dikirim", message: "Kode verifikasi dikirim",
@@ -57,16 +85,14 @@ export async function POST(req: Request) {
isRegistered: true, isRegistered: true,
}); });
} else { } else {
// ❌ User belum terdaftar → JANGAN kirim OTP
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: "Nomor belum terdaftar", message: "Nomor belum terdaftar",
isRegistered: false, isRegistered: false,
// Tidak ada kodeId
}); });
} }
} catch (error) { } catch (error) {
console.error("Error Login:", error); console.error("Error Login:", error);
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Terjadi kesalahan saat login" }, { success: false, message: "Terjadi kesalahan saat login" },
{ status: 500 } { status: 500 }

View File

@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { randomOTP } from '../_lib/randomOTP'; // pastikan ada import { randomOTP } from '../_lib/randomOTP';
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
@@ -36,7 +37,17 @@ export async function POST(req: Request) {
data: { nomor, otp: otpNumber, isActive: true } data: { nomor, otp: otpNumber, isActive: true }
}); });
// ✅ Kembalikan kodeId (jangan buat user di sini!) // ✅ Set cookie flow=register (Next.js 15+ syntax)
const cookieStore = await cookies();
cookieStore.set('auth_flow', 'register', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 5, // 5 menit
path: '/'
});
// ✅ Kembalikan kodeId
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Kode verifikasi dikirim', message: 'Kode verifikasi dikirim',

View File

@@ -0,0 +1,34 @@
// app/api/auth/set-flow/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function POST(request: Request) {
try {
const { flow } = await request.json();
if (!flow || !['login', 'register'].includes(flow)) {
return NextResponse.json(
{ success: false, message: 'Invalid flow parameter' },
{ status: 400 }
);
}
// ✅ Next.js 15 syntax
const cookieStore = await cookies();
cookieStore.set('auth_flow', flow, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 5,
path: '/'
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('❌ Error setting flow cookie:', error);
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
);
}
}