Fix registrasi, waitong-room, & tampilan layout sesuai id
This commit is contained in:
@@ -110,6 +110,7 @@ export default function Validasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Verify OTP
|
||||||
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' },
|
||||||
@@ -123,19 +124,25 @@ export default function Validasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Finalize registration
|
||||||
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor, username, kodeId }),
|
body: JSON.stringify({ nomor: cleanNomor, username, kodeId }),
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await finalizeRes.json();
|
const data = await finalizeRes.json();
|
||||||
|
|
||||||
if (data.success || finalizeRes.redirected) {
|
// ✅ Check JSON response (bukan redirect)
|
||||||
// ✅ Cleanup setelah registrasi sukses
|
if (data.success) {
|
||||||
|
toast.success('Registrasi berhasil! Menunggu persetujuan admin.');
|
||||||
await cleanupStorage();
|
await cleanupStorage();
|
||||||
window.location.href = '/waiting-room';
|
|
||||||
|
// ✅ Client-side redirect
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/waiting-room';
|
||||||
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || 'Registrasi gagal');
|
toast.error(data.message || 'Registrasi gagal');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,399 @@
|
|||||||
|
// 'use client'
|
||||||
|
|
||||||
|
// import colors from "@/con/colors";
|
||||||
|
// import { authStore } from "@/store/authStore";
|
||||||
|
// import {
|
||||||
|
// ActionIcon,
|
||||||
|
// AppShell,
|
||||||
|
// AppShellHeader,
|
||||||
|
// AppShellMain,
|
||||||
|
// AppShellNavbar,
|
||||||
|
// Burger,
|
||||||
|
// Center,
|
||||||
|
// Flex,
|
||||||
|
// Group,
|
||||||
|
// Image,
|
||||||
|
// Loader,
|
||||||
|
// NavLink,
|
||||||
|
// ScrollArea,
|
||||||
|
// Text,
|
||||||
|
// Tooltip,
|
||||||
|
// rem
|
||||||
|
// } from "@mantine/core";
|
||||||
|
// import { useDisclosure } from "@mantine/hooks";
|
||||||
|
// import {
|
||||||
|
// IconChevronLeft,
|
||||||
|
// IconChevronRight,
|
||||||
|
// IconLogout2
|
||||||
|
// } from "@tabler/icons-react";
|
||||||
|
// import _ from "lodash";
|
||||||
|
// import Link from "next/link";
|
||||||
|
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||||
|
// import { useEffect, useState } from "react";
|
||||||
|
// // import { useSnapshot } from "valtio";
|
||||||
|
// import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||||
|
|
||||||
|
// export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
// const [opened, { toggle }] = useDisclosure();
|
||||||
|
// const [loading, setLoading] = useState(true);
|
||||||
|
// const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
|
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
|
// const router = useRouter();
|
||||||
|
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||||
|
|
||||||
|
// // const { user } = useSnapshot(authStore);
|
||||||
|
|
||||||
|
// // console.log("Current user in store:", user);
|
||||||
|
|
||||||
|
// // ✅ FIX: Selalu fetch user data setiap kali komponen mount
|
||||||
|
// useEffect(() => {
|
||||||
|
// const fetchUser = async () => {
|
||||||
|
// try {
|
||||||
|
// const res = await fetch('/api/auth/me');
|
||||||
|
// const data = await res.json();
|
||||||
|
|
||||||
|
// if (data.user) {
|
||||||
|
// // ✅ Check if user is NOT active → redirect to waiting room
|
||||||
|
// if (!data.user.isActive) {
|
||||||
|
// authStore.setUser(null);
|
||||||
|
// router.replace('/waiting-room');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ✅ Fetch menuIds
|
||||||
|
// const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
||||||
|
// const menuData = await menuRes.json();
|
||||||
|
|
||||||
|
// const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
||||||
|
// ? [...menuData.menuIds]
|
||||||
|
// : null;
|
||||||
|
|
||||||
|
// // ✅ Set user dengan menuIds yang fresh
|
||||||
|
// authStore.setUser({
|
||||||
|
// id: data.user.id,
|
||||||
|
// name: data.user.name,
|
||||||
|
// roleId: Number(data.user.roleId),
|
||||||
|
// menuIds,
|
||||||
|
// isActive: data.user.isActive
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // ✅ TAMBAHKAN INI: Redirect ke dashboard sesuai roleId
|
||||||
|
// const currentPath = window.location.pathname;
|
||||||
|
// const expectedPath = getRedirectPath(Number(data.user.roleId));
|
||||||
|
|
||||||
|
// // Jika user di halaman /admin tapi bukan di path yang sesuai roleId
|
||||||
|
// if (currentPath === '/admin' || !currentPath.startsWith(expectedPath)) {
|
||||||
|
// router.replace(expectedPath);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// } else {
|
||||||
|
// authStore.setUser(null);
|
||||||
|
// router.replace('/login');
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Gagal memuat data pengguna:', error);
|
||||||
|
// authStore.setUser(null);
|
||||||
|
// router.replace('/login');
|
||||||
|
// } finally {
|
||||||
|
// setLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// fetchUser();
|
||||||
|
// }, [router]);
|
||||||
|
|
||||||
|
// // ✅ Fungsi helper untuk get redirect path
|
||||||
|
// const getRedirectPath = (roleId: number): string => {
|
||||||
|
// switch (roleId) {
|
||||||
|
// case 0: // DEVELOPER
|
||||||
|
// case 1: // SUPERADMIN
|
||||||
|
// case 2: // ADMIN_DESA
|
||||||
|
// return '/admin/landing-page/profil/program-inovasi';
|
||||||
|
// case 3: // ADMIN_KESEHATAN
|
||||||
|
// return '/admin/kesehatan/posyandu';
|
||||||
|
// case 4: // ADMIN_PENDIDIKAN
|
||||||
|
// return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
|
// default:
|
||||||
|
// return '/admin';
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (loading) {
|
||||||
|
// return (
|
||||||
|
// <AppShell>
|
||||||
|
// <AppShellMain>
|
||||||
|
// <Center h="100vh">
|
||||||
|
// <Loader />
|
||||||
|
// </Center>
|
||||||
|
// </AppShellMain>
|
||||||
|
// </AppShell>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ✅ Ambil menu berdasarkan roleId dan menuIds
|
||||||
|
// const currentNav = authStore.user
|
||||||
|
// ? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
||||||
|
// : [];
|
||||||
|
|
||||||
|
// const handleLogout = async () => {
|
||||||
|
// try {
|
||||||
|
// setIsLoggingOut(true);
|
||||||
|
|
||||||
|
// // ✅ Panggil API logout untuk clear session di server
|
||||||
|
// const response = await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
// const result = await response.json();
|
||||||
|
|
||||||
|
// if (result.success) {
|
||||||
|
// // Clear user data dari store
|
||||||
|
// authStore.setUser(null);
|
||||||
|
|
||||||
|
// // Clear localStorage
|
||||||
|
// localStorage.removeItem('auth_nomor');
|
||||||
|
// localStorage.removeItem('auth_kodeId');
|
||||||
|
|
||||||
|
// // Force reload untuk reset semua state
|
||||||
|
// window.location.href = '/login';
|
||||||
|
// } else {
|
||||||
|
// console.error('Logout failed:', result.message);
|
||||||
|
// // Tetap redirect meskipun gagal
|
||||||
|
// authStore.setUser(null);
|
||||||
|
// window.location.href = '/login';
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error during logout:', error);
|
||||||
|
// // Tetap clear store dan redirect jika error
|
||||||
|
// authStore.setUser(null);
|
||||||
|
// window.location.href = '/login';
|
||||||
|
// } finally {
|
||||||
|
// setIsLoggingOut(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <AppShell
|
||||||
|
// suppressHydrationWarning
|
||||||
|
// header={{ height: 64 }}
|
||||||
|
// navbar={{
|
||||||
|
// width: { base: 260, sm: 280, lg: 300 },
|
||||||
|
// breakpoint: 'sm',
|
||||||
|
// collapsed: {
|
||||||
|
// mobile: !opened,
|
||||||
|
// desktop: !desktopOpened,
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
// padding="md"
|
||||||
|
// >
|
||||||
|
// <AppShellHeader
|
||||||
|
// style={{
|
||||||
|
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||||
|
// borderBottom: `1px solid ${colors["blue-button"]}20`,
|
||||||
|
// padding: '0 16px',
|
||||||
|
// }}
|
||||||
|
// px={{ base: 'sm', sm: 'md' }}
|
||||||
|
// py={{ base: 'xs', sm: 'sm' }}
|
||||||
|
// >
|
||||||
|
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
|
||||||
|
// <Flex align="center" gap="sm">
|
||||||
|
// <Image
|
||||||
|
// src="/assets/images/darmasaba-icon.png"
|
||||||
|
// alt="Logo Darmasaba"
|
||||||
|
// w={{ base: 32, sm: 40 }}
|
||||||
|
// h={{ base: 32, sm: 40 }}
|
||||||
|
// radius="md"
|
||||||
|
// loading="lazy"
|
||||||
|
// style={{
|
||||||
|
// minWidth: '32px',
|
||||||
|
// height: 'auto',
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// <Text
|
||||||
|
// fw={700}
|
||||||
|
// c={colors["blue-button"]}
|
||||||
|
// fz={{ base: 'md', sm: 'xl' }}
|
||||||
|
// >
|
||||||
|
// Admin Darmasaba
|
||||||
|
// </Text>
|
||||||
|
// </Flex>
|
||||||
|
|
||||||
|
// <Group gap="xs">
|
||||||
|
// {!desktopOpened && (
|
||||||
|
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
||||||
|
// <ActionIcon
|
||||||
|
// variant="light"
|
||||||
|
// radius="xl"
|
||||||
|
// size="lg"
|
||||||
|
// onClick={toggleDesktop}
|
||||||
|
// color={colors["blue-button"]}
|
||||||
|
// >
|
||||||
|
// <IconChevronRight />
|
||||||
|
// </ActionIcon>
|
||||||
|
// </Tooltip>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// <Burger
|
||||||
|
// opened={opened}
|
||||||
|
// onClick={toggle}
|
||||||
|
// hiddenFrom="sm"
|
||||||
|
// size="md"
|
||||||
|
// color={colors["blue-button"]}
|
||||||
|
// mr="xs"
|
||||||
|
// />
|
||||||
|
|
||||||
|
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
||||||
|
// <ActionIcon
|
||||||
|
// onClick={() => {
|
||||||
|
// router.push("/darmasaba");
|
||||||
|
// }}
|
||||||
|
// color={colors["blue-button"]}
|
||||||
|
// radius="xl"
|
||||||
|
// size="lg"
|
||||||
|
// variant="gradient"
|
||||||
|
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||||
|
// >
|
||||||
|
// <Image
|
||||||
|
// src="/assets/images/darmasaba-icon.png"
|
||||||
|
// alt="Logo Darmasaba"
|
||||||
|
// w={20}
|
||||||
|
// h={20}
|
||||||
|
// radius="md"
|
||||||
|
// loading="lazy"
|
||||||
|
// style={{
|
||||||
|
// minWidth: '20px',
|
||||||
|
// height: 'auto',
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// </ActionIcon>
|
||||||
|
// </Tooltip>
|
||||||
|
// <Tooltip label="Keluar" position="bottom" withArrow>
|
||||||
|
// <ActionIcon
|
||||||
|
// onClick={handleLogout}
|
||||||
|
// color={colors["blue-button"]}
|
||||||
|
// radius="xl"
|
||||||
|
// size="lg"
|
||||||
|
// variant="gradient"
|
||||||
|
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||||
|
// loading={isLoggingOut}
|
||||||
|
// disabled={isLoggingOut}
|
||||||
|
// >
|
||||||
|
// <IconLogout2 size={22} />
|
||||||
|
// </ActionIcon>
|
||||||
|
// </Tooltip>
|
||||||
|
// </Group>
|
||||||
|
// </Group>
|
||||||
|
// </AppShellHeader>
|
||||||
|
|
||||||
|
// <AppShellNavbar
|
||||||
|
// component={ScrollArea}
|
||||||
|
// style={{
|
||||||
|
// background: "#ffffff",
|
||||||
|
// borderRight: `1px solid ${colors["blue-button"]}20`,
|
||||||
|
// }}
|
||||||
|
// p={{ base: 'xs', sm: 'sm' }}
|
||||||
|
// >
|
||||||
|
// <AppShell.Section p="sm">
|
||||||
|
// {currentNav.map((v, k) => {
|
||||||
|
// const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <NavLink
|
||||||
|
// key={k}
|
||||||
|
// defaultOpened={isParentActive}
|
||||||
|
// c={isParentActive ? colors["blue-button"] : "gray"}
|
||||||
|
// label={
|
||||||
|
// <Text fw={isParentActive ? 600 : 400} fz="sm">
|
||||||
|
// {v.name}
|
||||||
|
// </Text>
|
||||||
|
// }
|
||||||
|
// style={{
|
||||||
|
// borderRadius: rem(10),
|
||||||
|
// marginBottom: rem(4),
|
||||||
|
// transition: "background 150ms ease",
|
||||||
|
// }}
|
||||||
|
// styles={{
|
||||||
|
// root: {
|
||||||
|
// '&:hover': {
|
||||||
|
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
// variant="light"
|
||||||
|
// active={isParentActive}
|
||||||
|
// >
|
||||||
|
// {v.children.map((child, key) => {
|
||||||
|
// const isChildActive = segments.includes(
|
||||||
|
// _.lowerCase(child.name)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <NavLink
|
||||||
|
// key={key}
|
||||||
|
// href={child.path}
|
||||||
|
// c={isChildActive ? colors["blue-button"] : "gray"}
|
||||||
|
// label={
|
||||||
|
// <Text fw={isChildActive ? 600 : 400} fz="sm">
|
||||||
|
// {child.name}
|
||||||
|
// </Text>
|
||||||
|
// }
|
||||||
|
// styles={{
|
||||||
|
// root: {
|
||||||
|
// borderRadius: rem(8),
|
||||||
|
// marginBottom: rem(2),
|
||||||
|
// transition: 'background 150ms ease',
|
||||||
|
// padding: '6px 12px',
|
||||||
|
// '&:hover': {
|
||||||
|
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
||||||
|
// },
|
||||||
|
// ...(isChildActive && {
|
||||||
|
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||||
|
// }),
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
// active={isChildActive}
|
||||||
|
// component={Link}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
// </NavLink>
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
// </AppShell.Section>
|
||||||
|
|
||||||
|
// <AppShell.Section py="md">
|
||||||
|
// <Group justify="end" pr="sm">
|
||||||
|
// <Tooltip
|
||||||
|
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
||||||
|
// position="top"
|
||||||
|
// withArrow
|
||||||
|
// >
|
||||||
|
// <ActionIcon
|
||||||
|
// variant="light"
|
||||||
|
// radius="xl"
|
||||||
|
// size="lg"
|
||||||
|
// onClick={toggleDesktop}
|
||||||
|
// color={colors["blue-button"]}
|
||||||
|
// >
|
||||||
|
// <IconChevronLeft />
|
||||||
|
// </ActionIcon>
|
||||||
|
// </Tooltip>
|
||||||
|
// </Group>
|
||||||
|
// </AppShell.Section>
|
||||||
|
// </AppShellNavbar>
|
||||||
|
|
||||||
|
// <AppShellMain
|
||||||
|
// style={{
|
||||||
|
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
||||||
|
// minHeight: "100vh",
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// {children}
|
||||||
|
// </AppShellMain>
|
||||||
|
// </AppShell>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// app/admin/layout.tsx
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
@@ -30,7 +426,6 @@ import _ from "lodash";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
// import { useSnapshot } from "valtio";
|
|
||||||
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
@@ -41,41 +436,51 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||||
|
|
||||||
// const { user } = useSnapshot(authStore);
|
|
||||||
|
|
||||||
// console.log("Current user in store:", user);
|
|
||||||
|
|
||||||
// ✅ FIX: Selalu fetch user data setiap kali komponen mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/me');
|
const res = await fetch('/api/auth/me', {
|
||||||
|
credentials: 'include' // ✅ ADD credentials
|
||||||
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
// Check if user is active
|
// ✅ Check if user is NOT active → redirect to waiting room
|
||||||
if (!data.user.isActive) {
|
if (!data.user.isActive) {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
router.replace('/waiting-room');
|
router.replace('/waiting-room');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ PENTING: Selalu fetch menuIds terbaru setiap login
|
// ✅ Fetch menuIds
|
||||||
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
|
||||||
|
credentials: 'include' // ✅ ADD credentials
|
||||||
|
});
|
||||||
const menuData = await menuRes.json();
|
const menuData = await menuRes.json();
|
||||||
|
|
||||||
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
||||||
? [...menuData.menuIds]
|
? [...menuData.menuIds]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// ✅ Set user dengan menuIds yang fresh dari database
|
// ✅ Set user dengan menuIds yang fresh
|
||||||
authStore.setUser({
|
authStore.setUser({
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
name: data.user.name,
|
name: data.user.name,
|
||||||
roleId: Number(data.user.roleId),
|
roleId: Number(data.user.roleId),
|
||||||
menuIds, // menuIds terbaru
|
menuIds,
|
||||||
isActive: data.user.isActive
|
isActive: data.user.isActive
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ✅ IMPROVED: Redirect ONLY if di root /admin
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
|
if (currentPath === '/admin') {
|
||||||
|
const expectedPath = getRedirectPath(Number(data.user.roleId));
|
||||||
|
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
||||||
|
router.replace(expectedPath);
|
||||||
|
}
|
||||||
|
// ✅ Jangan redirect jika user sudah di path yang valid
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
router.replace('/login');
|
router.replace('/login');
|
||||||
@@ -90,7 +495,22 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}, [router]); // ✅ Hapus dependency pada authStore.user
|
}, [router]); // ✅ Only depend on router
|
||||||
|
|
||||||
|
const getRedirectPath = (roleId: number): string => {
|
||||||
|
switch (roleId) {
|
||||||
|
case 0: // DEVELOPER
|
||||||
|
case 1: // SUPERADMIN
|
||||||
|
case 2: // ADMIN_DESA
|
||||||
|
return '/admin/landing-page/profil/program-inovasi';
|
||||||
|
case 3: // ADMIN_KESEHATAN
|
||||||
|
return '/admin/kesehatan/posyandu';
|
||||||
|
case 4: // ADMIN_PENDIDIKAN
|
||||||
|
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
|
default:
|
||||||
|
return '/admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -104,7 +524,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Ambil menu berdasarkan roleId dan menuIds
|
|
||||||
const currentNav = authStore.user
|
const currentNav = authStore.user
|
||||||
? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
||||||
: [];
|
: [];
|
||||||
@@ -113,29 +532,25 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
setIsLoggingOut(true);
|
setIsLoggingOut(true);
|
||||||
|
|
||||||
// ✅ Panggil API logout untuk clear session di server
|
const response = await fetch('/api/auth/logout', {
|
||||||
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
method: 'POST',
|
||||||
|
credentials: 'include' // ✅ ADD credentials
|
||||||
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Clear user data dari store
|
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
|
|
||||||
// Clear localStorage
|
|
||||||
localStorage.removeItem('auth_nomor');
|
localStorage.removeItem('auth_nomor');
|
||||||
localStorage.removeItem('auth_kodeId');
|
localStorage.removeItem('auth_kodeId');
|
||||||
|
localStorage.removeItem('auth_username');
|
||||||
// Force reload untuk reset semua state
|
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
} else {
|
} else {
|
||||||
console.error('Logout failed:', result.message);
|
console.error('Logout failed:', result.message);
|
||||||
// Tetap redirect meskipun gagal
|
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during logout:', error);
|
console.error('Error during logout:', error);
|
||||||
// Tetap clear store dan redirect jika error
|
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -157,6 +572,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
|
{/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */}
|
||||||
<AppShellHeader
|
<AppShellHeader
|
||||||
style={{
|
style={{
|
||||||
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||||
@@ -175,16 +591,9 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
h={{ base: 32, sm: 40 }}
|
h={{ base: 32, sm: 40 }}
|
||||||
radius="md"
|
radius="md"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{
|
style={{ minWidth: '32px', height: 'auto' }}
|
||||||
minWidth: '32px',
|
|
||||||
height: 'auto',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text fw={700} c={colors["blue-button"]} fz={{ base: 'md', sm: 'xl' }}>
|
||||||
fw={700}
|
|
||||||
c={colors["blue-button"]}
|
|
||||||
fz={{ base: 'md', sm: 'xl' }}
|
|
||||||
>
|
|
||||||
Admin Darmasaba
|
Admin Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -192,63 +601,22 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{!desktopOpened && (
|
{!desktopOpened && (
|
||||||
<Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
<Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
||||||
<ActionIcon
|
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={colors["blue-button"]}>
|
||||||
variant="light"
|
|
||||||
radius="xl"
|
|
||||||
size="lg"
|
|
||||||
onClick={toggleDesktop}
|
|
||||||
color={colors["blue-button"]}
|
|
||||||
>
|
|
||||||
<IconChevronRight />
|
<IconChevronRight />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Burger
|
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="md" color={colors["blue-button"]} mr="xs" />
|
||||||
opened={opened}
|
|
||||||
onClick={toggle}
|
|
||||||
hiddenFrom="sm"
|
|
||||||
size="md"
|
|
||||||
color={colors["blue-button"]}
|
|
||||||
mr="xs"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
<Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
||||||
<ActionIcon
|
<ActionIcon onClick={() => router.push("/darmasaba")} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }}>
|
||||||
onClick={() => {
|
<Image src="/assets/images/darmasaba-icon.png" alt="Logo Darmasaba" w={20} h={20} radius="md" loading="lazy" style={{ minWidth: '20px', height: 'auto' }} />
|
||||||
router.push("/darmasaba");
|
|
||||||
}}
|
|
||||||
color={colors["blue-button"]}
|
|
||||||
radius="xl"
|
|
||||||
size="lg"
|
|
||||||
variant="gradient"
|
|
||||||
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src="/assets/images/darmasaba-icon.png"
|
|
||||||
alt="Logo Darmasaba"
|
|
||||||
w={20}
|
|
||||||
h={20}
|
|
||||||
radius="md"
|
|
||||||
loading="lazy"
|
|
||||||
style={{
|
|
||||||
minWidth: '20px',
|
|
||||||
height: 'auto',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||||
<ActionIcon
|
<ActionIcon onClick={handleLogout} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }} loading={isLoggingOut} disabled={isLoggingOut}>
|
||||||
onClick={handleLogout}
|
|
||||||
color={colors["blue-button"]}
|
|
||||||
radius="xl"
|
|
||||||
size="lg"
|
|
||||||
variant="gradient"
|
|
||||||
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
|
||||||
loading={isLoggingOut}
|
|
||||||
disabled={isLoggingOut}
|
|
||||||
>
|
|
||||||
<IconLogout2 size={22} />
|
<IconLogout2 size={22} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -256,75 +624,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</Group>
|
</Group>
|
||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
|
|
||||||
<AppShellNavbar
|
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
||||||
component={ScrollArea}
|
{/* ... Navbar content sama seperti sebelumnya ... */}
|
||||||
style={{
|
|
||||||
background: "#ffffff",
|
|
||||||
borderRight: `1px solid ${colors["blue-button"]}20`,
|
|
||||||
}}
|
|
||||||
p={{ base: 'xs', sm: 'sm' }}
|
|
||||||
>
|
|
||||||
<AppShell.Section p="sm">
|
<AppShell.Section p="sm">
|
||||||
{currentNav.map((v, k) => {
|
{currentNav.map((v, k) => {
|
||||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink key={k} defaultOpened={isParentActive} c={isParentActive ? colors["blue-button"] : "gray"} label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>} style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} variant="light" active={isParentActive}>
|
||||||
key={k}
|
|
||||||
defaultOpened={isParentActive}
|
|
||||||
c={isParentActive ? colors["blue-button"] : "gray"}
|
|
||||||
label={
|
|
||||||
<Text fw={isParentActive ? 600 : 400} fz="sm">
|
|
||||||
{v.name}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
borderRadius: rem(10),
|
|
||||||
marginBottom: rem(4),
|
|
||||||
transition: "background 150ms ease",
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
variant="light"
|
|
||||||
active={isParentActive}
|
|
||||||
>
|
|
||||||
{v.children.map((child, key) => {
|
{v.children.map((child, key) => {
|
||||||
const isChildActive = segments.includes(
|
const isChildActive = segments.includes(_.lowerCase(child.name));
|
||||||
_.lowerCase(child.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink key={key} href={child.path} c={isChildActive ? colors["blue-button"] : "gray"} label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>} styles={{ root: { borderRadius: rem(8), marginBottom: rem(2), transition: 'background 150ms ease', padding: '6px 12px', '&:hover': { backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' }, ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) } }} active={isChildActive} component={Link} />
|
||||||
key={key}
|
|
||||||
href={child.path}
|
|
||||||
c={isChildActive ? colors["blue-button"] : "gray"}
|
|
||||||
label={
|
|
||||||
<Text fw={isChildActive ? 600 : 400} fz="sm">
|
|
||||||
{child.name}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
borderRadius: rem(8),
|
|
||||||
marginBottom: rem(2),
|
|
||||||
transition: 'background 150ms ease',
|
|
||||||
padding: '6px 12px',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
|
||||||
},
|
|
||||||
...(isChildActive && {
|
|
||||||
backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
active={isChildActive}
|
|
||||||
component={Link}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -334,18 +644,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
<AppShell.Section py="md">
|
<AppShell.Section py="md">
|
||||||
<Group justify="end" pr="sm">
|
<Group justify="end" pr="sm">
|
||||||
<Tooltip
|
<Tooltip label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"} position="top" withArrow>
|
||||||
label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={colors["blue-button"]}>
|
||||||
position="top"
|
|
||||||
withArrow
|
|
||||||
>
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
radius="xl"
|
|
||||||
size="lg"
|
|
||||||
onClick={toggleDesktop}
|
|
||||||
color={colors["blue-button"]}
|
|
||||||
>
|
|
||||||
<IconChevronLeft />
|
<IconChevronLeft />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -353,12 +653,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</AppShell.Section>
|
</AppShell.Section>
|
||||||
</AppShellNavbar>
|
</AppShellNavbar>
|
||||||
|
|
||||||
<AppShellMain
|
<AppShellMain style={{ background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)", minHeight: "100vh" }}>
|
||||||
style={{
|
|
||||||
background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
|
||||||
minHeight: "100vh",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</AppShellMain>
|
</AppShellMain>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
|
// Verify OTP
|
||||||
|
const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
|
where: { id: kodeId },
|
||||||
|
select: { nomor: true, isActive: true }
|
||||||
|
});
|
||||||
|
|
||||||
if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
|
if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "OTP tidak valid" },
|
{ success: false, message: "OTP tidak valid" },
|
||||||
@@ -38,6 +43,7 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check duplicate username
|
||||||
if (await prisma.user.findFirst({ where: { username } })) {
|
if (await prisma.user.findFirst({ where: { username } })) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Username sudah digunakan" },
|
{ success: false, message: "Username sudah digunakan" },
|
||||||
@@ -45,12 +51,20 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Tentukan roleId sebagai STRING
|
// Check duplicate nomor
|
||||||
const targetRoleId = "1"; // ✅ string, bukan number
|
if (await prisma.user.findUnique({ where: { nomor: cleanNomor } })) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Nomor sudah terdaftar" },
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Validasi role (gunakan string)
|
// 🔥 Tentukan roleId sebagai STRING
|
||||||
|
const targetRoleId = "2"; // ✅ Default ADMIN_DESA (roleId "2")
|
||||||
|
|
||||||
|
// Validasi role exists
|
||||||
const roleExists = await prisma.role.findUnique({
|
const roleExists = await prisma.role.findUnique({
|
||||||
where: { id: targetRoleId }, // ✅ id bertipe string
|
where: { id: targetRoleId },
|
||||||
select: { id: true }
|
select: { id: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,17 +75,17 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buat user dengan roleId string
|
// ✅ Create user (inactive, waiting approval)
|
||||||
const newUser = await prisma.user.create({
|
const newUser = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
username,
|
username,
|
||||||
nomor,
|
nomor: cleanNomor,
|
||||||
roleId: targetRoleId, // ✅ string
|
roleId: targetRoleId,
|
||||||
isActive: false,
|
isActive: false, // Waiting for admin approval
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Berikan akses menu
|
// ✅ Berikan akses menu default based on role
|
||||||
const menuIds = DEFAULT_MENUS_BY_ROLE[targetRoleId] || [];
|
const menuIds = DEFAULT_MENUS_BY_ROLE[targetRoleId] || [];
|
||||||
if (menuIds.length > 0) {
|
if (menuIds.length > 0) {
|
||||||
await prisma.userMenuAccess.createMany({
|
await prisma.userMenuAccess.createMany({
|
||||||
@@ -79,14 +93,17 @@ export async function POST(req: Request) {
|
|||||||
userId: newUser.id,
|
userId: newUser.id,
|
||||||
menuId,
|
menuId,
|
||||||
})),
|
})),
|
||||||
|
skipDuplicates: true, // ✅ Avoid duplicate errors
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Mark OTP as used
|
||||||
await prisma.kodeOtp.update({
|
await prisma.kodeOtp.update({
|
||||||
where: { id: kodeId },
|
where: { id: kodeId },
|
||||||
data: { isActive: false },
|
data: { isActive: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ✅ Create session token
|
||||||
const token = await sessionCreate({
|
const token = await sessionCreate({
|
||||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
@@ -95,25 +112,35 @@ export async function POST(req: Request) {
|
|||||||
id: newUser.id,
|
id: newUser.id,
|
||||||
nomor: newUser.nomor,
|
nomor: newUser.nomor,
|
||||||
username: newUser.username,
|
username: newUser.username,
|
||||||
roleId: newUser.roleId, // string
|
roleId: newUser.roleId,
|
||||||
isActive: false,
|
isActive: false, // User belum aktif
|
||||||
},
|
},
|
||||||
invalidatePrevious: false,
|
invalidatePrevious: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = NextResponse.redirect(new URL('/waiting-room', req.url));
|
// ✅ PENTING: Return JSON response (bukan redirect)
|
||||||
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
|
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,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
sameSite: 'lax',
|
||||||
path: "/",
|
path: "/",
|
||||||
maxAge: 30 * 24 * 60 * 60,
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Finalize Registration Error:", error);
|
console.error("❌ Finalize Registration Error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Registrasi gagal" },
|
{ success: false, message: "Registrasi gagal. Silakan coba lagi." },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
|
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
|
||||||
|
|
||||||
async function fetchUser() {
|
async function fetchUser() {
|
||||||
const res = await fetch('/api/auth/me');
|
const res = await fetch('/api/auth/me', {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
throw new Error(`HTTP ${res.status}: ${text}`);
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user