Login, Register, Verifkasi Code Admin V1
This commit is contained in:
1127
prisma/migrations/20251119062255_add_unique_username/migration.sql
Normal file
1127
prisma/migrations/20251119062255_add_unique_username/migration.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2164,7 +2164,7 @@ enum StatusPeminjaman {
|
|||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
username String
|
username String @unique
|
||||||
nomor String @unique
|
nomor String @unique
|
||||||
role Role @relation(fields: [roleId], references: [id])
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
roleId String @default("1")
|
roleId String @default("1")
|
||||||
|
|||||||
@@ -1,104 +1,98 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
|
import { apiFetchLogin } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PhoneInput } from "react-international-phone";
|
import { PhoneInput } from 'react-international-phone';
|
||||||
import "react-international-phone/style.css";
|
import 'react-international-phone/style.css';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const [phone, setPhone] = useState("")
|
const [phone, setPhone] = useState('');
|
||||||
const [isError, setError] = useState(false)
|
const [loading, setLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
|
// Login.tsx
|
||||||
async function onLogin() {
|
async function onLogin() {
|
||||||
const nomor = phone.substring(1);
|
const cleanPhone = phone.replace(/\D/g, '');
|
||||||
if (nomor.length <= 4) return setError(true)
|
if (cleanPhone.length < 10) {
|
||||||
|
toast.error('Nomor telepon tidak valid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await apiFetchLogin({ nomor: nomor })
|
const response = await apiFetchLogin({ nomor: cleanPhone });
|
||||||
if (response && response.success) {
|
|
||||||
localStorage.setItem("hipmi_auth_code_id", response.kodeId);
|
if (!response.success) {
|
||||||
toast.success(response.message);
|
toast.error(response.message || 'Gagal memproses login');
|
||||||
router.push("/validasi", { scroll: false });
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan nomor untuk register
|
||||||
|
localStorage.setItem('auth_nomor', cleanPhone);
|
||||||
|
|
||||||
|
if (response.isRegistered) {
|
||||||
|
// ✅ User lama: simpan kodeId & ke validasi
|
||||||
|
localStorage.setItem('auth_kodeId', response.kodeId);
|
||||||
|
router.push('/validasi');
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
// ❌ User baru: langsung ke registrasi (tanpa kodeId)
|
||||||
toast.error(response?.message);
|
router.push('/registrasi');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false)
|
console.error('Error Login:', error);
|
||||||
console.log("Error Login", error)
|
toast.error('Terjadi kesalahan saat login');
|
||||||
toast.error("Terjadi kesalahan saat login")
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg}>
|
<Stack pos="relative" bg={colors.Bg}>
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||||
<Stack align='center' justify='center' h={"100vh"}>
|
<Stack align="center" justify="center" h="100vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center' gap={"lg"}>
|
<Stack align="center" gap="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title ta="center" order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Login
|
Login
|
||||||
</Title>
|
</Title>
|
||||||
<Center>
|
<Center>
|
||||||
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
|
<Image
|
||||||
|
loading="lazy"
|
||||||
|
src="/darmasaba-icon.png"
|
||||||
|
alt="Logo"
|
||||||
|
w={80}
|
||||||
|
h={80}
|
||||||
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
{/* <Box mb={10}>
|
|
||||||
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masuk Untuk Akses Admin</Text>
|
|
||||||
<TextInput
|
|
||||||
label='Username'
|
|
||||||
placeholder='Username'
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Box> */}
|
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
countrySelectorStyleProps={{
|
countrySelectorStyleProps={{
|
||||||
buttonStyle: {
|
buttonStyle: {
|
||||||
backgroundColor: colors['blue-button'],
|
backgroundColor: colors['blue-button'],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
inputStyle={{ width: "100%"}}
|
inputStyle={{ width: '100%' }}
|
||||||
defaultCountry="id"
|
defaultCountry="id"
|
||||||
onChange={(val) => {
|
value={phone}
|
||||||
setPhone(val);
|
onChange={(val) => setPhone(val)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isError ? (
|
|
||||||
toast.error("Masukan nomor telepon anda")
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<Box py={20}>
|
<Box py={20}>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
radius={'xl'}
|
radius="xl"
|
||||||
onClick={onLogin}
|
onClick={onLogin}
|
||||||
loading={loading ? true : false}
|
loading={loading}
|
||||||
>Masuk
|
>
|
||||||
|
Masuk
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex justify={'center'} align={'center'}>
|
|
||||||
<Text>Belum punya akun? </Text>
|
|
||||||
<Button variant='transparent' component={Link} href={'/registrasi'}>
|
|
||||||
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,113 +1,127 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
// app/registrasi/page.tsx
|
||||||
'use client'
|
'use client';
|
||||||
import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
|
|
||||||
|
import { apiFetchRegister } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { PhoneInput } from "react-international-phone";
|
import { PhoneInput } from 'react-international-phone';
|
||||||
import "react-international-phone/style.css";
|
import 'react-international-phone/style.css';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
function Registrasi() {
|
export default function Registrasi() {
|
||||||
const [phone, setPhone] = useState("")
|
const router = useRouter();
|
||||||
const router = useRouter()
|
const [username, setUsername] = useState('');
|
||||||
const [value, setValue] = useState("")
|
|
||||||
const [isValue, setIsValue] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
||||||
|
|
||||||
async function onRegistarsi() {
|
// Ambil data dari localStorage (dari login)
|
||||||
if (value.length < 5) {
|
useEffect(() => {
|
||||||
toast.error("Username minimal 5 karakter!");
|
const storedNomor = localStorage.getItem('auth_nomor');
|
||||||
|
if (!storedNomor) {
|
||||||
|
toast.error('Akses tidak valid');
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPhone(storedNomor);
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!username || username.trim().length < 5) {
|
||||||
|
toast.error('Username minimal 5 karakter!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (username.includes(' ')) {
|
||||||
|
toast.error('Username tidak boleh ada spasi!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.includes(" ")) {
|
const cleanPhone = phone.replace(/\D/g, '');
|
||||||
toast.error("Username tidak boleh ada spasi!");
|
if (cleanPhone.length < 10) {
|
||||||
return;
|
toast.error('Nomor tidak valid!');
|
||||||
}
|
|
||||||
|
|
||||||
if (!phone) {
|
|
||||||
toast.error("Nomor telepon wajib diisi!");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const respone = await apiFetchRegister({ nomor: phone, username: value });
|
// ✅ Hanya kirim username & nomor → dapat kodeId
|
||||||
|
const response = await apiFetchRegister({ username, nomor: cleanPhone });
|
||||||
|
|
||||||
if (respone.success) {
|
if (response.success) {
|
||||||
router.push("/login", { scroll: false });
|
// Simpan sementara
|
||||||
toast.success(respone.message);
|
localStorage.setItem('auth_kodeId', response.kodeId);
|
||||||
|
localStorage.setItem('auth_username', username); // simpan username
|
||||||
|
|
||||||
} else {
|
toast.success('Kode verifikasi dikirim!');
|
||||||
setLoading(false);
|
router.push('/validasi'); // ✅ ke halaman validasi
|
||||||
toast.error(respone.message);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error Registrasi:', error);
|
||||||
|
toast.error('Gagal mengirim OTP');
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
console.log("Error Registrasi", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} gap={"22"} py={"xl"} h={"100vh"}>
|
<Stack pos="relative" bg={colors.Bg} gap="22" py="xl" h="100vh">
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Stack justify='center' align='center' h={"80vh"}>
|
<Stack justify="center" align="center" h="80vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center'>
|
<Stack align="center">
|
||||||
<Title order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Registrasi
|
Registrasi
|
||||||
</Title>
|
</Title>
|
||||||
<Center>
|
<Center>
|
||||||
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
|
<Image loading="lazy" src="/darmasaba-icon.png" alt="" w={80} />
|
||||||
</Center>
|
</Center>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
<TextInput placeholder='Username'
|
<TextInput
|
||||||
label='Username'
|
label="Username"
|
||||||
maxLength={50}
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.currentTarget.value)}
|
||||||
error={
|
error={
|
||||||
value.length > 0 && value.length < 5
|
username.length > 0 && username.length < 5
|
||||||
? "Minimal 5 karakter !"
|
? 'Minimal 5 karakter!'
|
||||||
: value.includes(" ")
|
: username.includes(' ')
|
||||||
? "Tidak boleh ada spasi"
|
? 'Tidak boleh ada spasi'
|
||||||
: isValue
|
: ''
|
||||||
? "Masukan username anda"
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
onChange={(val) => {
|
|
||||||
val.currentTarget.value.length > 0 ? setIsValue(false) : "";
|
|
||||||
setValue(val.currentTarget.value);
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
/>
|
<Box pt="md">
|
||||||
<Box py={10}>
|
<Text fz="sm">Nomor Telepon</Text>
|
||||||
<Text fz={"sm"} >Nomor Telepon</Text>
|
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
countrySelectorStyleProps={{
|
|
||||||
buttonStyle: {
|
|
||||||
backgroundColor: colors['blue-button'],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
inputStyle={{ width: "100%" }}
|
|
||||||
defaultCountry="id"
|
defaultCountry="id"
|
||||||
onChange={(val) => {
|
value={phone}
|
||||||
setPhone(val);
|
disabled
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box pb={10}>
|
|
||||||
<Checkbox
|
<Box pt="md">
|
||||||
label="Saya menyetujui syarat dan ketentuan yang berlaku"
|
<Checkbox label="Saya menyetujui syarat dan ketentuan" defaultChecked />
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box pb={20} >
|
|
||||||
<Button fullWidth bg={colors['blue-button']} radius={'xl'} onClick={onRegistarsi} loading={loading ? true : false}>Daftar</Button>
|
<Box pt="xl">
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
radius="xl"
|
||||||
|
onClick={handleRegister}
|
||||||
|
loading={loading}
|
||||||
|
disabled={username.length < 5}
|
||||||
|
>
|
||||||
|
Kirim Kode Verifikasi
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -117,5 +131,3 @@ function Registrasi() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Registrasi;
|
|
||||||
|
|||||||
@@ -1,31 +1,177 @@
|
|||||||
'use client'
|
// app/validasi/page.tsx
|
||||||
import colors from '@/con/colors';
|
'use client';
|
||||||
import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
function Validasi() {
|
import { apiFetchOtpData, apiFetchVerifyOtp } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
const router = useRouter()
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Loader, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export default function Validasi() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [nomor, setNomor] = useState<string | null>(null);
|
||||||
|
const [otp, setOtp] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [kodeId, setKodeId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedKodeId = localStorage.getItem('auth_kodeId');
|
||||||
|
if (!storedKodeId) {
|
||||||
|
toast.error('Akses tidak valid');
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setKodeId(storedKodeId);
|
||||||
|
|
||||||
|
const fetchOtpData = async () => {
|
||||||
|
try {
|
||||||
|
const result = await apiFetchOtpData({ kodeId: storedKodeId });
|
||||||
|
if (result.success && result.data?.nomor) {
|
||||||
|
setNomor(result.data.nomor);
|
||||||
|
} else {
|
||||||
|
throw new Error('OTP tidak valid');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal muat OTP:', error);
|
||||||
|
toast.error('Kode verifikasi tidak valid');
|
||||||
|
router.push('/login');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOtpData();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
if (!kodeId || !nomor || otp.length < 4) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId });
|
||||||
|
|
||||||
|
if (verifyResult.success) {
|
||||||
|
cleanupStorage();
|
||||||
|
router.push('/admin/landing-page/profil/program-inovasi');
|
||||||
|
return; // ✅ HENTIKAN eksekusi di sini
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hanya coba registrasi jika akun tidak ditemukan
|
||||||
|
if (verifyResult.status === 404 && verifyResult.message?.includes('Akun tidak ditemukan')) {
|
||||||
|
const username = localStorage.getItem('auth_username');
|
||||||
|
if (!username) {
|
||||||
|
toast.error('Data registrasi hilang');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regRes = await fetch('/api/auth/finalize-registration', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor, username, otp, kodeId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const regData = await regRes.json();
|
||||||
|
if (regData.success) {
|
||||||
|
cleanupStorage();
|
||||||
|
router.push('/admin/landing-page/profil/program-inovasi');
|
||||||
|
} else {
|
||||||
|
toast.error(regData.message || 'Registrasi gagal');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hanya tampilkan error jika bukan kasus "akun tidak ditemukan"
|
||||||
|
toast.error(verifyResult.message || 'Verifikasi gagal');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Verifikasi error:', error);
|
||||||
|
toast.error('Terjadi kesalahan');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupStorage = () => {
|
||||||
|
localStorage.removeItem('auth_kodeId');
|
||||||
|
localStorage.removeItem('auth_nomor');
|
||||||
|
localStorage.removeItem('auth_username');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResend = async () => {
|
||||||
|
if (!nomor) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/resend-otp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
localStorage.setItem('auth_kodeId', data.kodeId);
|
||||||
|
toast.success('OTP baru dikirim');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal kirim ulang');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg}>
|
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
|
||||||
|
<Loader size="md" color={colors['blue-button']} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nomor) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack pos="relative" bg={colors.Bg}>
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||||
<Stack align='center' justify='center' h={"100vh"}>
|
<Stack align="center" justify="center" h="100vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center' gap={"lg"}>
|
<Stack align="center" gap="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title ta="center" order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Kode Verifikasi
|
Kode Verifikasi
|
||||||
</Title>
|
</Title>
|
||||||
|
<Text ta="center" size="sm" c="dimmed" mt="xs">
|
||||||
|
Kami telah mengirim kode ke nomor <strong>{nomor}</strong>
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Box mb={10}>
|
<Box mb={20}>
|
||||||
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masukkan Kode Verifikasi</Text>
|
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
||||||
<PinInput type={/^[0-9]*$/} inputType="tel" inputMode="numeric" />
|
Masukkan Kode Verifikasi
|
||||||
|
</Text>
|
||||||
|
<PinInput
|
||||||
|
length={4}
|
||||||
|
value={otp}
|
||||||
|
onChange={setOtp}
|
||||||
|
onComplete={handleVerify}
|
||||||
|
inputMode="numeric"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box py={20} >
|
|
||||||
<Button onClick={() => router.push("/admin/landing-page/profile/program-inovasi")}>
|
<Button
|
||||||
Page
|
fullWidth
|
||||||
|
onClick={handleVerify}
|
||||||
|
loading={loading}
|
||||||
|
disabled={otp.length < 4}
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
Verifikasi
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
|
||||||
|
<Text ta="center" size="sm" mt="md">
|
||||||
|
Tidak menerima kode?{' '}
|
||||||
|
<Button variant="subtle" onClick={handleResend} size="xs" p={0} h="auto" color={colors['blue-button']}>
|
||||||
|
Kirim Ulang
|
||||||
|
</Button>
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -34,5 +180,3 @@ function Validasi() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Validasi;
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
export {
|
|
||||||
apiFetchLogin,
|
|
||||||
apiFetchRegister
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
|
|
||||||
const response = await fetch("/api/auth/login", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ nomor: nomor }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await response.json().catch(() => null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiFetchRegister = async ({
|
|
||||||
nomor,
|
|
||||||
username,
|
|
||||||
}: {
|
|
||||||
nomor: string;
|
|
||||||
username: string;
|
|
||||||
}) => {
|
|
||||||
const data = {
|
|
||||||
username: username,
|
|
||||||
nomor: nomor,
|
|
||||||
};
|
|
||||||
const respone = await fetch("/api/auth/register", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ data }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await respone.json();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
// return await respone.json().catch(() => null);
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -12,49 +11,67 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const codeOtp = randomOTP();
|
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { nomor } = body;
|
const { nomor } = body;
|
||||||
const res = await fetch(
|
|
||||||
`https://wa.wibudev.com/code?nom=${nomor}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.
|
|
||||||
\n
|
|
||||||
>> Kode OTP anda: ${codeOtp}.
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendWa = await res.json();
|
if (!nomor || typeof nomor !== "string") {
|
||||||
|
|
||||||
if (sendWa.status !== "success")
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Nomor Whatsapp Tidak Aktif" },
|
{ success: false, message: "Nomor tidak valid" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const createOtpId = await prisma.kodeOtp.create({
|
// Cek apakah user sudah terdaftar
|
||||||
data: {
|
const existingUser = await prisma.user.findUnique({
|
||||||
nomor: nomor,
|
where: { nomor },
|
||||||
otp: codeOtp,
|
select: { id: true }, // cukup cek ada/tidak
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createOtpId)
|
const isRegistered = !!existingUser;
|
||||||
|
|
||||||
|
// Generate OTP
|
||||||
|
const codeOtp = randomOTP(); // Pastikan ini menghasilkan number (sesuai tipe di KodeOtp.otp: Int)
|
||||||
|
|
||||||
|
// Kirim OTP via WA
|
||||||
|
const waRes = await fetch(
|
||||||
|
`https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.%0A%0A>> Kode OTP anda: ${codeOtp}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendWa = await waRes.json();
|
||||||
|
|
||||||
|
if (sendWa.status !== "success") {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Gagal mengirim kode OTP" },
|
{ success: false, message: "Nomor WhatsApp tidak aktif" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan OTP ke database
|
||||||
|
const otpRecord = await prisma.kodeOtp.create({
|
||||||
|
data: {
|
||||||
|
nomor: nomor,
|
||||||
|
otp: codeOtp, // Pastikan tipe ini number (Int di Prisma = number di TS)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: true,
|
success: true,
|
||||||
message: "Kode verifikasi terkirim",
|
message: "Kode verifikasi terkirim",
|
||||||
kodeId: createOtpId.id,
|
kodeId: otpRecord.id,
|
||||||
|
isRegistered, // 🔑 Ini kunci untuk frontend tahu harus ke register atau verifikasi
|
||||||
},
|
},
|
||||||
{ status: 200 }
|
{ status: 200 }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error Login", error);
|
console.error("Error Login:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Terjadi masalah saat login" , reason: error as Error },
|
{
|
||||||
|
success: false,
|
||||||
|
message: "Terjadi masalah saat login",
|
||||||
|
// Hindari mengirim error mentah ke client di production
|
||||||
|
// reason: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,
|
||||||
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
30
src/app/api/[[...slugs]]/_lib/auth/me/route.ts
Normal file
30
src/app/api/[[...slugs]]/_lib/auth/me/route.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
// Jika pakai custom session (bukan next-auth), ganti dengan logic session-mu
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
// 🔸 GANTI DENGAN LOGIC SESSION-MU
|
||||||
|
// Contoh: jika kamu simpan user.id di cookie atau JWT
|
||||||
|
const userId = req.cookies.get("hipmi_user_id")?.value; // sesuaikan
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
nomor: true,
|
||||||
|
isActive: true,
|
||||||
|
role: { select: { name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json({ error: "User not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ user });
|
||||||
|
}
|
||||||
104
src/app/api/[[...slugs]]/_lib/auth/register/route.ts
Normal file
104
src/app/api/[[...slugs]]/_lib/auth/register/route.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// import prisma from "@/lib/prisma";
|
||||||
|
// import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
// export async function POST(req: Request) {
|
||||||
|
// if (req.method !== "POST") {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Method Not Allowed" },
|
||||||
|
// { status: 405 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const { username, nomor, otp, kodeId } = await req.json();
|
||||||
|
|
||||||
|
// // Validasi input
|
||||||
|
// if (!username || !nomor || !otp || !kodeId) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Data tidak lengkap" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 1. Verifikasi OTP
|
||||||
|
// const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
|
// where: { id: kodeId },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!otpRecord) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Kode verifikasi tidak valid" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!otpRecord.isActive) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Kode verifikasi sudah digunakan atau kadaluarsa" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (otpRecord.otp !== otp) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Kode OTP salah" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (otpRecord.nomor !== nomor) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Nomor tidak sesuai dengan kode verifikasi" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 3. Cek apakah nomor sudah terdaftar
|
||||||
|
// const existingUserByNomor = await prisma.user.findUnique({
|
||||||
|
// where: { nomor },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (existingUserByNomor) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Nomor sudah terdaftar" },
|
||||||
|
// { status: 409 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 4. Buat user
|
||||||
|
// const newUser = await prisma.user.create({
|
||||||
|
// data: {
|
||||||
|
// username,
|
||||||
|
// nomor,
|
||||||
|
// // roleId default "1" (sesuai model)
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // 5. Nonaktifkan OTP agar tidak bisa dipakai lagi
|
||||||
|
// await prisma.kodeOtp.update({
|
||||||
|
// where: { id: kodeId },
|
||||||
|
// data: { isActive: false },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return NextResponse.json(
|
||||||
|
// {
|
||||||
|
// success: true,
|
||||||
|
// message: "Registrasi berhasil",
|
||||||
|
// userId: newUser.id,
|
||||||
|
// },
|
||||||
|
// { status: 201 }
|
||||||
|
// );
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error registrasi:", error);
|
||||||
|
// return NextResponse.json(
|
||||||
|
// {
|
||||||
|
// success: false,
|
||||||
|
// message: "Terjadi kesalahan saat registrasi",
|
||||||
|
// // reason: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,
|
||||||
|
// },
|
||||||
|
// { status: 500 }
|
||||||
|
// );
|
||||||
|
// } finally {
|
||||||
|
// await prisma.$disconnect();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
152
src/app/api/auth/_lib/api_fetch_auth.ts
Normal file
152
src/app/api/auth/_lib/api_fetch_auth.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
// app/api/auth/_lib/api_fetch_auth.ts
|
||||||
|
|
||||||
|
// app/api/auth/_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/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ nomor: cleanPhone }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pastikan respons bisa di-parse sebagai JSON
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Non-JSON response from /api/auth/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/auth/send-otp-register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }),
|
||||||
|
});
|
||||||
|
|
||||||
|
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/auth/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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const apiFetchVerifyOtp = async ({
|
||||||
|
// nomor,
|
||||||
|
// otp,
|
||||||
|
// kodeId
|
||||||
|
// }: {
|
||||||
|
// nomor: string;
|
||||||
|
// otp: string;
|
||||||
|
// kodeId: string;
|
||||||
|
// }) => {
|
||||||
|
// if (!nomor || !otp || !kodeId) {
|
||||||
|
// throw new Error('Data verifikasi tidak lengkap');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!/^\d{4,6}$/.test(otp)) {
|
||||||
|
// throw new Error('Kode OTP harus 4-6 digit angka');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const response = await fetch('/api/auth/verify-otp', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const data = await response.json();
|
||||||
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// throw new Error(data.message || 'Verifikasi OTP gagal');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return data;
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const apiFetchVerifyOtp = async ({
|
||||||
|
nomor,
|
||||||
|
otp,
|
||||||
|
kodeId
|
||||||
|
}: {
|
||||||
|
nomor: string;
|
||||||
|
otp: string;
|
||||||
|
kodeId: string;
|
||||||
|
}) => {
|
||||||
|
if (!nomor || !otp || !kodeId) {
|
||||||
|
throw new Error('Data verifikasi tidak lengkap');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d{4,6}$/.test(otp)) {
|
||||||
|
throw new Error('Kode OTP harus 4-6 digit angka');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/auth/verify-otp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// ✅ Jangan throw error untuk status 4xx — biarkan frontend handle
|
||||||
|
return {
|
||||||
|
success: response.ok,
|
||||||
|
...data,
|
||||||
|
status: response.status,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,50 +1,32 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { jwtVerify } from "jose";
|
import { jwtVerify } from "jose";
|
||||||
|
|
||||||
export async function decrypt({
|
export async function decrypt({
|
||||||
token,
|
token,
|
||||||
encodedKey,
|
jwtSecret,
|
||||||
}: {
|
}: {
|
||||||
token: string;
|
token: string;
|
||||||
encodedKey: string;
|
jwtSecret: string;
|
||||||
}): Promise<Record<string, any> | null> {
|
}): Promise<Record<string, unknown> | null> {
|
||||||
if (!token || !encodedKey) {
|
if (!token || !jwtSecret) return null;
|
||||||
console.error("Missing required parameters:", {
|
|
||||||
hasToken: !!token,
|
|
||||||
hasEncodedKey: !!encodedKey,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const enc = new TextEncoder().encode(encodedKey);
|
const secret = new TextEncoder().encode(jwtSecret);
|
||||||
const { payload } = await jwtVerify(token, enc, {
|
const { payload } = await jwtVerify(token, secret, {
|
||||||
algorithms: ["HS256"],
|
algorithms: ["HS256"],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!payload || !payload.user) {
|
if (
|
||||||
console.error("Invalid payload structure:", {
|
typeof payload !== "object" ||
|
||||||
hasPayload: !!payload,
|
payload === null ||
|
||||||
hasUser: payload ? !!payload.user : false,
|
!("user" in payload) ||
|
||||||
});
|
typeof payload.user !== "object"
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging untuk debug
|
return payload.user as Record<string, unknown>;
|
||||||
// console.log("Decrypt successful:", {
|
|
||||||
// payloadExists: !!payload,
|
|
||||||
// userExists: !!payload.user,
|
|
||||||
// tokenPreview: token.substring(0, 10) + "...",
|
|
||||||
// });
|
|
||||||
|
|
||||||
return payload.user as Record<string, any>;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Token verification failed:", {
|
console.error("JWT Decrypt failed:", error);
|
||||||
error,
|
|
||||||
tokenLength: token?.length,
|
|
||||||
errorName: error instanceof Error ? error.name : "Unknown error",
|
|
||||||
errorMessage: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
|
|
||||||
export async function encrypt({
|
export async function encrypt({
|
||||||
user,
|
user,
|
||||||
exp = "7 year",
|
exp = "7d",
|
||||||
encodedKey,
|
jwtSecret,
|
||||||
}: {
|
}: {
|
||||||
user: Record<string, any>;
|
user: Record<string, unknown>;
|
||||||
exp?: string;
|
exp?: string | number;
|
||||||
encodedKey: string;
|
jwtSecret: string;
|
||||||
}): Promise<string | null> {
|
}): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const enc = new TextEncoder().encode(encodedKey);
|
const secret = new TextEncoder().encode(jwtSecret);
|
||||||
return new SignJWT({ user })
|
return new SignJWT({ user })
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setIssuedAt()
|
.setIssuedAt()
|
||||||
.setExpirationTime(exp)
|
.setExpirationTime(exp)
|
||||||
.sign(enc);
|
.sign(secret);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal mengenkripsi", error);
|
console.error("JWT Encrypt failed:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wibu:0.2.82
|
|
||||||
|
|||||||
@@ -1,36 +1,42 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { encrypt } from "./encrypt";
|
import { encrypt } from "./encrypt";
|
||||||
|
|
||||||
export async function sessionCreate({
|
export async function sessionCreate({
|
||||||
sessionKey,
|
sessionKey,
|
||||||
exp = "7 year",
|
exp = "7 year",
|
||||||
encodedKey,
|
jwtSecret,
|
||||||
user,
|
user,
|
||||||
}: {
|
}: {
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
exp?: string;
|
exp?: string;
|
||||||
encodedKey: string;
|
jwtSecret: string;
|
||||||
user: Record<string, unknown>;
|
user: Record<string, unknown>;
|
||||||
}) {
|
}) {
|
||||||
|
// 🔒 Validasi kunci tidak kosong
|
||||||
|
if (!sessionKey || sessionKey.length === 0) {
|
||||||
|
throw new Error("sessionKey tidak boleh kosong");
|
||||||
|
}
|
||||||
|
if (!jwtSecret || jwtSecret.length === 0) {
|
||||||
|
throw new Error("jwtSecret tidak boleh kosong");
|
||||||
|
}
|
||||||
|
|
||||||
const token = await encrypt({
|
const token = await encrypt({
|
||||||
exp,
|
exp,
|
||||||
encodedKey,
|
jwtSecret,
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cookie: any = {
|
if (token === null) {
|
||||||
key: sessionKey,
|
throw new Error("Token generation failed");
|
||||||
value: token,
|
}
|
||||||
options: {
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set(sessionKey, token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
path: "/",
|
path: "/",
|
||||||
},
|
secure: process.env.NODE_ENV === "production",
|
||||||
};
|
});
|
||||||
|
|
||||||
(await cookies()).set(cookie.key, cookie.value, { ...cookie.options });
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// wibu:0.2.82
|
|
||||||
|
|||||||
40
src/app/api/auth/finalize-registration/route.ts
Normal file
40
src/app/api/auth/finalize-registration/route.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// app/api/auth/finalize-registration/route.ts
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { sessionCreate } from "../_lib/session_create";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { nomor, username, kodeId } = await req.json();
|
||||||
|
|
||||||
|
// Verifikasi OTP (sama seperti verify-otp)
|
||||||
|
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
|
||||||
|
if (!otpRecord?.isActive || otpRecord.nomor !== nomor) {
|
||||||
|
return NextResponse.json({ success: false, message: 'OTP tidak valid' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat user
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: { username, nomor, isActive: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nonaktifkan OTP
|
||||||
|
await prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } });
|
||||||
|
|
||||||
|
// Buat session
|
||||||
|
const token = await sessionCreate({
|
||||||
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
|
user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = NextResponse.json({ success: true, roleId: user.roleId });
|
||||||
|
response.cookies.set(process.env.BASE_SESSION_KEY!, token, { /* options */ });
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Finalize Registration Error:', error);
|
||||||
|
return NextResponse.json({ success: false, message: 'Registrasi gagal' }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
// app/api/auth/login/route.ts
|
||||||
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";
|
||||||
|
|
||||||
@@ -12,49 +12,63 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const codeOtp = randomOTP();
|
const { nomor } = await req.json();
|
||||||
const body = await req.json();
|
|
||||||
const { nomor } = body;
|
|
||||||
const res = await fetch(
|
|
||||||
`https://wa.wibudev.com/code?nom=${nomor}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.
|
|
||||||
\n
|
|
||||||
>> Kode OTP anda: ${codeOtp}.
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendWa = await res.json();
|
if (!nomor || typeof nomor !== "string") {
|
||||||
|
|
||||||
if (sendWa.status !== "success")
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Nomor Whatsapp Tidak Aktif" },
|
{ success: false, message: "Nomor tidak valid" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const createOtpId = await prisma.kodeOtp.create({
|
const existingUser = await prisma.user.findUnique({
|
||||||
data: {
|
where: { nomor },
|
||||||
nomor: nomor,
|
select: { id: true, isActive: true },
|
||||||
otp: codeOtp,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createOtpId)
|
const isRegistered = !!existingUser;
|
||||||
|
|
||||||
|
if (isRegistered) {
|
||||||
|
// ✅ User terdaftar → kirim OTP
|
||||||
|
const codeOtp = randomOTP();
|
||||||
|
const otpNumber = Number(codeOtp);
|
||||||
|
|
||||||
|
const waMessage = `Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
|
||||||
|
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
|
||||||
|
|
||||||
|
const res = await fetch(waUrl);
|
||||||
|
const sendWa = await res.json();
|
||||||
|
|
||||||
|
if (sendWa.status !== "success") {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Gagal mengirim kode OTP" },
|
{ success: false, message: "Gagal mengirim OTP via WhatsApp" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
const createOtpId = await prisma.kodeOtp.create({
|
||||||
{
|
data: { nomor, otp: otpNumber, isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Kode verifikasi terkirim",
|
message: "Kode verifikasi dikirim",
|
||||||
kodeId: createOtpId.id,
|
kodeId: createOtpId.id,
|
||||||
},
|
isRegistered: true,
|
||||||
{ status: 200 }
|
});
|
||||||
);
|
} else {
|
||||||
|
// ❌ User belum terdaftar → JANGAN kirim OTP
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Nomor belum terdaftar",
|
||||||
|
isRegistered: false,
|
||||||
|
// Tidak ada kodeId
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error Login", error);
|
console.error("Error Login:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Terjadi masalah saat login" , reason: error as Error },
|
{ success: false, message: "Terjadi kesalahan saat login" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
30
src/app/api/auth/me/route.ts
Normal file
30
src/app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
// Jika pakai custom session (bukan next-auth), ganti dengan logic session-mu
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
// 🔸 GANTI DENGAN LOGIC SESSION-MU
|
||||||
|
// Contoh: jika kamu simpan user.id di cookie atau JWT
|
||||||
|
const userId = req.cookies.get("desadarmasaba_user_id")?.value; // sesuaikan
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
nomor: true,
|
||||||
|
isActive: true,
|
||||||
|
role: { select: { name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json({ error: "User not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ user });
|
||||||
|
}
|
||||||
41
src/app/api/auth/otp-data/route.ts
Normal file
41
src/app/api/auth/otp-data/route.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// app/api/auth/otp-data/route.ts
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { kodeId } = await req.json();
|
||||||
|
|
||||||
|
if (!kodeId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode ID tidak diberikan" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
|
where: { id: kodeId },
|
||||||
|
select: { id: true, nomor: true, isActive: true, createdAt: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!otpRecord || !otpRecord.isActive) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode verifikasi tidak valid atau sudah digunakan" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: otpRecord,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching OTP data:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Gagal mengambil data OTP" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +1,119 @@
|
|||||||
import prisma from "@/lib/prisma";
|
// app/api/auth/register/route.ts
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from 'next/server';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
if (req.method !== "POST") {
|
try {
|
||||||
|
// Terima langsung properti, bukan { data: { ... } }
|
||||||
|
const { username, nomor } = await req.json();
|
||||||
|
|
||||||
|
// Validasi input
|
||||||
|
if (!username || !nomor) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Method Not Allowed" },
|
{ success: false, message: 'Data tidak lengkap' },
|
||||||
{ status: 405 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// // Validasi OTP: pastikan berisi digit saja
|
||||||
const { data } = await req.json();
|
// const cleanOtp = otp.toString().trim();
|
||||||
|
// if (!/^\d{4,6}$/.test(cleanOtp)) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: 'Kode OTP tidak valid' },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
const cekUsername = await prisma.user.findUnique({
|
// const receivedOtp = parseInt(cleanOtp, 10);
|
||||||
where: {
|
// if (isNaN(receivedOtp)) {
|
||||||
username: data.username,
|
// return NextResponse.json(
|
||||||
nomor: data.nomor,
|
// { success: false, message: 'Kode OTP tidak valid' },
|
||||||
},
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Cari OTP record
|
||||||
|
// const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
|
// where: { id: kodeId },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!otpRecord) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: 'Kode verifikasi tidak valid' },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!otpRecord.isActive) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: 'Kode verifikasi sudah kadaluarsa' },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (otpRecord.otp !== receivedOtp) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: 'Kode OTP salah' },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (otpRecord.nomor !== nomor) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: 'Nomor tidak sesuai' },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Cek duplikat nomor
|
||||||
|
const existingUser = await prisma.user.findUnique({
|
||||||
|
where: { nomor },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cekUsername)
|
if (existingUser) {
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
success: false,
|
{ success: false, message: 'Nomor sudah terdaftar' },
|
||||||
message: "Username sudah digunakan",
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek username unik (pastikan ada @unique di schema!)
|
||||||
|
const existingByUsername = await prisma.user.findUnique({
|
||||||
|
where: { username },
|
||||||
});
|
});
|
||||||
|
|
||||||
const createUser = await prisma.user.create({
|
if (existingByUsername) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Username sudah digunakan' },
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat user
|
||||||
|
const newUser = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
username: data.username,
|
username: username.trim(),
|
||||||
nomor: data.nomor,
|
nomor,
|
||||||
|
isActive: false,
|
||||||
|
// roleId default "1"
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createUser)
|
// // Nonaktifkan OTP
|
||||||
return NextResponse.json(
|
// await prisma.kodeOtp.update({
|
||||||
{ success: false, message: "Gagal Registrasi" },
|
// where: { id: kodeId },
|
||||||
{ status: 500 }
|
// data: { isActive: false },
|
||||||
);
|
// });
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json({
|
||||||
{
|
|
||||||
success: true,
|
success: true,
|
||||||
message: "Registrasi Berhasil, Anda Sedang Login",
|
message: 'Pendaftaran berhasil. Menunggu persetujuan admin.',
|
||||||
// data: createUser,
|
userId: newUser.id,
|
||||||
},
|
});
|
||||||
{ status: 201 }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error registrasi:", error);
|
console.error('Registration error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ success: false, message: 'Terjadi kesalahan saat pendaftaran' },
|
||||||
success: false,
|
|
||||||
message: "Maaf, Terjadi Keselahan",
|
|
||||||
reason: (error as Error).message,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
71
src/app/api/auth/resend/route.ts
Normal file
71
src/app/api/auth/resend/route.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { randomOTP } from "../_lib/randomOTP";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Method Not Allowed" },
|
||||||
|
{ status: 405 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const codeOtp = randomOTP();
|
||||||
|
const body = await req.json();
|
||||||
|
const { nomor } = body;
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`https://wa.wibudev.com/code?nom=${nomor}&text=HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya.
|
||||||
|
\n
|
||||||
|
>> Kode OTP anda: ${codeOtp}.
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendWa = await res.json();
|
||||||
|
if (sendWa.status !== "success")
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: "Nomor Whatsapp Tidak Aktif",
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const createOtpId = await prisma.kodeOtp.create({
|
||||||
|
data: {
|
||||||
|
nomor: nomor,
|
||||||
|
otp: codeOtp,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createOtpId)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: "Gagal Membuat Kode OTP",
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
message: "Kode Verifikasi Dikirim",
|
||||||
|
kodeId: createOtpId.id,
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(" Error Resend OTP", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: "Server Whatsapp Error !!",
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/app/api/auth/send-otp-register/route.ts
Normal file
51
src/app/api/auth/send-otp-register/route.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { randomOTP } from "../_lib/randomOTP";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { username, nomor } = await req.json();
|
||||||
|
|
||||||
|
if (!username || !nomor) {
|
||||||
|
return NextResponse.json({ success: false, message: 'Data tidak lengkap' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek duplikat
|
||||||
|
if (await prisma.user.findUnique({ where: { nomor } })) {
|
||||||
|
return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 });
|
||||||
|
}
|
||||||
|
if (await prisma.user.findUnique({ where: { username } })) {
|
||||||
|
return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate OTP
|
||||||
|
const codeOtp = randomOTP();
|
||||||
|
const otpNumber = Number(codeOtp);
|
||||||
|
|
||||||
|
// Kirim WA
|
||||||
|
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
|
||||||
|
const res = await fetch(waUrl);
|
||||||
|
const sendWa = await res.json();
|
||||||
|
|
||||||
|
if (sendWa.status !== "success") {
|
||||||
|
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan OTP
|
||||||
|
const otpRecord = await prisma.kodeOtp.create({
|
||||||
|
data: { nomor, otp: otpNumber, isActive: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Kode verifikasi dikirim',
|
||||||
|
kodeId: otpRecord.id,
|
||||||
|
nomor,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send OTP for Register Error:', error);
|
||||||
|
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/app/api/auth/validasi/route.ts
Normal file
78
src/app/api/auth/validasi/route.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { sessionCreate } from "../_lib/session_create";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Method Not Allowed" },
|
||||||
|
{ status: 405 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { nomor } = await req.json();
|
||||||
|
const dataUser = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
nomor: nomor,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
nomor: true,
|
||||||
|
username: true,
|
||||||
|
roleId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dataUser == null)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Nomor Belum Terdaftar" },
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = await sessionCreate({
|
||||||
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
|
user: dataUser as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Gagal membuat session" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Buat response dengan token dalam cookie
|
||||||
|
const response = NextResponse.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil Login",
|
||||||
|
roleId: dataUser.roleId,
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set cookie dengan token yang sudah dipastikan tidak null
|
||||||
|
response.cookies.set(process.env.NEXT_PUBLIC_BASE_SESSION_KEY!, token, {
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik (1 bulan)
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API Error or Server Error", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: "Maaf, Terjadi Keselahan",
|
||||||
|
reason: (error as Error).message,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/app/api/auth/verify-otp/route.ts
Normal file
139
src/app/api/auth/verify-otp/route.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// app/api/auth/verify-otp/route.ts
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { sessionCreate } from "../_lib/session_create";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Method Not Allowed" },
|
||||||
|
{ status: 405 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { nomor, otp, kodeId } = await req.json();
|
||||||
|
|
||||||
|
// Validasi input
|
||||||
|
if (!nomor || !otp || !kodeId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Data tidak lengkap" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cari OTP record
|
||||||
|
const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
|
where: { id: kodeId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!otpRecord) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode verifikasi tidak valid" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!otpRecord.isActive) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode verifikasi sudah digunakan" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pastikan tipe data cocok (OTP di DB = number)
|
||||||
|
const receivedOtp = Number(otp);
|
||||||
|
if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode OTP salah" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otpRecord.nomor !== nomor) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Nomor tidak sesuai" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek user berdasarkan nomor
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { nomor },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
nomor: true,
|
||||||
|
username: true,
|
||||||
|
roleId: true,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Akun tidak ditemukan" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isActive) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Akun belum disetujui oleh admin" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat session
|
||||||
|
const token = await sessionCreate({
|
||||||
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
|
jwtSecret: process.env.BASE_TOKEN_KEY!, // ✅
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
nomor: user.nomor,
|
||||||
|
username: user.username,
|
||||||
|
roleId: user.roleId,
|
||||||
|
isActive: user.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Gagal membuat session" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nonaktifkan OTP
|
||||||
|
await prisma.kodeOtp.update({
|
||||||
|
where: { id: kodeId },
|
||||||
|
data: { isActive: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set cookie & respons
|
||||||
|
const response = NextResponse.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil login",
|
||||||
|
roleId: user.roleId,
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
|
||||||
|
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
httpOnly: true, // 🔒 lebih aman
|
||||||
|
maxAge: 30 * 24 * 60 * 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Verify OTP Error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Terjadi kesalahan saat verifikasi" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/app/waiting-room/page.tsx
Normal file
92
src/app/waiting-room/page.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Center, Loader, Paper, Stack, Text, Title } from '@mantine/core';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
// Ganti ini jika tidak pakai next-auth
|
||||||
|
async function fetchUser() {
|
||||||
|
const res = await fetch('/api/auth/me');
|
||||||
|
if (!res.ok) throw new Error('Unauthorized');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WaitingRoom() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [user, setUser] = useState<any>(null);
|
||||||
|
// const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchUser();
|
||||||
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
setUser(data.user);
|
||||||
|
|
||||||
|
// Jika sudah aktif, redirect ke dashboard admin
|
||||||
|
if (data.user.isActive) {
|
||||||
|
clearInterval(interval);
|
||||||
|
router.push('/admin'); // atau /dashboard
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
if (!isMounted) return;
|
||||||
|
setError(err.message || 'Gagal memuat status');
|
||||||
|
clearInterval(interval);
|
||||||
|
// Redirect ke login jika unauthorized
|
||||||
|
if (err.message === 'Unauthorized') {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000); // Cek setiap 2 detik
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Center h="100vh">
|
||||||
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={400}>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<Title order={3} c="red">Error</Title>
|
||||||
|
<Text>{error}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center h="100vh" bg={colors.Bg}>
|
||||||
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
|
||||||
|
<Stack align="center" gap="lg">
|
||||||
|
<Title order={2} c={colors['blue-button']} ta="center">
|
||||||
|
Menunggu Persetujuan
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Text ta="center" c="dimmed">
|
||||||
|
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text ta="center" size="sm" c="dimmed">
|
||||||
|
Nomor: {user?.nomor || '...'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Loader size="sm" color={colors['blue-button']} />
|
||||||
|
|
||||||
|
<Text ta="center" size="xs" c="dimmed">
|
||||||
|
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user