feat: update intern routes and navigation to use '/internship' path; refactor token handling in API and authentication components
This commit is contained in:
+1
-1
@@ -109,7 +109,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Intern only */}
|
{/* Intern only */}
|
||||||
<Route element={<ProtectedRoutes allowedRoles={["intern"]} />}>
|
<Route element={<ProtectedRoutes allowedRoles={["intern"]} />}>
|
||||||
<Route path="/intern" element={<InternLayout />}>
|
<Route path="/internship" element={<InternLayout />}>
|
||||||
<Route index element={<HomeInternPage />} />
|
<Route index element={<HomeInternPage />} />
|
||||||
<Route path="projects" element={<ProjectsPage />} />
|
<Route path="projects" element={<ProjectsPage />} />
|
||||||
<Route path="certificates" element={<SertificateList />} />
|
<Route path="certificates" element={<SertificateList />} />
|
||||||
|
|||||||
@@ -42,15 +42,15 @@ const NameInternBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
|||||||
|
|
||||||
const breadcrumbRoutes = [
|
const breadcrumbRoutes = [
|
||||||
// --- AREA INTERN ---
|
// --- AREA INTERN ---
|
||||||
{ path: '/intern', breadcrumb: 'Home' },
|
{ path: '/internship', breadcrumb: 'Home' },
|
||||||
{ path: '/intern/projects', breadcrumb: 'Projects' },
|
{ path: '/internship/projects', breadcrumb: 'Projects' },
|
||||||
{ path: '/intern/projects/:slug', breadcrumb: ProjectBreadcrumb },
|
{ path: '/internship/projects/:slug', breadcrumb: ProjectBreadcrumb },
|
||||||
{ path: '/intern/projects/:slug/:taskSlug', breadcrumb: TaskBreadcrumb },
|
{ path: '/internship/projects/:slug/:taskSlug', breadcrumb: TaskBreadcrumb },
|
||||||
{ path: '/intern/certificates', breadcrumb: 'Certificates' },
|
{ path: '/internship/certificates', breadcrumb: 'Certificates' },
|
||||||
{ path: '/intern/certificates/:slug', breadcrumb: CertBreadcrumb },
|
{ path: '/internship/certificates/:slug', breadcrumb: CertBreadcrumb },
|
||||||
{ path: '/intern/faq', breadcrumb: 'FAQ' },
|
{ path: '/internship/faq', breadcrumb: 'FAQ' },
|
||||||
{ path: '/intern/notifications', breadcrumb: 'Notifications' },
|
{ path: '/internship/notifications', breadcrumb: 'Notifications' },
|
||||||
{ path: '/intern/settings', breadcrumb: 'Settings' },
|
{ path: '/internship/settings', breadcrumb: 'Settings' },
|
||||||
|
|
||||||
// --- AREA MENTOR / ADMIN ---
|
// --- AREA MENTOR / ADMIN ---
|
||||||
{ path: '/mentor', breadcrumb: 'Home' },
|
{ path: '/mentor', breadcrumb: 'Home' },
|
||||||
@@ -71,7 +71,7 @@ export default function Breadcrumbs(){
|
|||||||
const breadcrumbs = useBreadcrumbs(breadcrumbRoutes);
|
const breadcrumbs = useBreadcrumbs(breadcrumbRoutes);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const rootPaths = ['/intern', '/mentor'];
|
const rootPaths = ['/internship', '/mentor'];
|
||||||
const isOnRootPath = rootPaths.includes(location.pathname);
|
const isOnRootPath = rootPaths.includes(location.pathname);
|
||||||
const fixbreadcumbs = breadcrumbs.filter(({ match }) => {
|
const fixbreadcumbs = breadcrumbs.filter(({ match }) => {
|
||||||
if (match.pathname === '/') return false;
|
if (match.pathname === '/') return false;
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { decodeJWT } from "../utils/decodeJWT";
|
|
||||||
import logo from "../../assets/logo.png";
|
import logo from "../../assets/logo.png";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Mail, Lock, Eye, EyeOff } from 'lucide-react';
|
import { Mail, Lock, Eye, EyeOff } from 'lucide-react';
|
||||||
import { useLogin } from "../../hooks/useUser";
|
import { useLogin } from "../../hooks/useUser";
|
||||||
import { customToast } from "../utils/showToast";
|
import { customToast } from "../utils/showToast";
|
||||||
import { CircleQuestionMark } from 'lucide-react';
|
import { CircleQuestionMark } from 'lucide-react';
|
||||||
|
import { decodeJWT } from "../utils/decodeJWT";
|
||||||
interface JwtPayload {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
role: "intern" | "mentor" | "admin";
|
|
||||||
signature?: string | null;
|
|
||||||
iat: number;
|
|
||||||
exp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InternifyLogin() {
|
export default function InternifyLogin() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -40,28 +31,18 @@ export default function InternifyLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await login(email, password);
|
const data = await login(email, password);
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
if (!data?.token) {
|
const { token } = data;
|
||||||
setError("Token login tidak ditemukan.");
|
document.cookie = `token=${token}; path=/; SameSite=Strict`;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoded = decodeJWT<JwtPayload>(data.token);
|
const decoded = decodeJWT(token);
|
||||||
const role = decoded?.role;
|
const role = decoded?.role;
|
||||||
|
|
||||||
if (!role) {
|
|
||||||
setError("Token login tidak valid.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionStorage.setItem("token", data.token);
|
|
||||||
|
|
||||||
if (role === "intern") {
|
if (role === "intern") {
|
||||||
navigate("/intern", { replace: true });
|
navigate("/internship");
|
||||||
} else if (role === "mentor" || role === "admin") {
|
} else if (role === "mentor" || role === "admin") {
|
||||||
navigate("/mentor", { replace: true });
|
navigate("/mentor");
|
||||||
} else {
|
} else {
|
||||||
sessionStorage.removeItem("token");
|
|
||||||
setError("Role user tidak dikenali.");
|
setError("Role user tidak dikenali.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="col-span-full flex flex-col items-center justify-center py-2 gap-3 mt-19">
|
<div className="col-span-full flex flex-col items-center py-2 gap-3 mt-19">
|
||||||
<img className="w-[440px]" src={TeamPana} alt="" />
|
<img className="w-[440px]" src={TeamPana} alt="" />
|
||||||
<p className="text-[18px] text-font-shade">Kamu belum tergabung ke projek apapun</p>
|
<p className="text-[18px] text-font-shade">Kamu belum tergabung ke projek apapun</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { MdOutlineAssignment, MdLogout } from "react-icons/md";
|
|||||||
import { HiUser } from "react-icons/hi2";
|
import { HiUser } from "react-icons/hi2";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: "Home", icon: GrHomeRounded, path: "/intern" },
|
{ label: "Home", icon: GrHomeRounded, path: "/internship" },
|
||||||
{ label: "List Projects", icon: MdOutlineAssignment, path: "/intern/projects" },
|
{ label: "List Projects", icon: MdOutlineAssignment, path: "/internship/projects" },
|
||||||
{ label: "Certificates", icon: Award, path: "/intern/certificates" },
|
{ label: "Certificates", icon: Award, path: "/internship/certificates" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const prefItems = [
|
const prefItems = [
|
||||||
{ label: "Settings", icon: Settings, path: "/intern/settings" },
|
{ label: "Settings", icon: Settings, path: "/internship/settings" },
|
||||||
{ label: "FAQ", icon: HelpCircle, path: "/intern/faq" },
|
{ label: "FAQ", icon: HelpCircle, path: "/internship/faq" },
|
||||||
{ label: "Logout", icon: MdLogout, path: "/login-internify", danger: true, isLogout: true },
|
{ label: "Logout", icon: MdLogout, path: "/login-internify", danger: true, isLogout: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
document.cookie = "token=; path=/; max-age=86400; SameSite=Strict";
|
document.cookie = "token=; Max-Age=0; path=/; SameSite=Strict";
|
||||||
|
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
navigate("/login-internify", { replace: true });
|
navigate("/login-internify", { replace: true });
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function SidebarMentor({ isOpen, closeSidebar }: SidebarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
document.cookie = "token=; path=/; max-age=86400; SameSite=Strict";
|
document.cookie = "token=; Max-Age=0; path=/; SameSite=Strict";
|
||||||
|
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
navigate("/login-internify", { replace: true });
|
navigate("/login-internify", { replace: true });
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const PrivateRoute = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
navigate("/humic-20242025-2");
|
navigate("/login-admin");
|
||||||
}
|
}
|
||||||
}, [navigate, token]);
|
}, [navigate, token]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,30 @@
|
|||||||
import { Outlet, Navigate } from "react-router-dom";
|
import { Outlet, Navigate } from "react-router-dom";
|
||||||
import { decodeJWT } from "./decodeJWT";
|
import { decodeJWT } from "../utils/decodeJWT";
|
||||||
|
|
||||||
type Role = "intern" | "admin" | "mentor";
|
interface PrivateRouteProps {
|
||||||
interface RouteProps {
|
allowedRoles?: ("intern" | "admin" | "mentor")[];
|
||||||
allowedRoles?: Role[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JwtPayload {
|
const ProtectedRoutes = ({ allowedRoles }: PrivateRouteProps) => {
|
||||||
id: string;
|
const token = document.cookie
|
||||||
email: string;
|
.split("; ")
|
||||||
role: "intern" | "mentor" | "admin";
|
.find((row) => row.startsWith("token="))
|
||||||
signature?: string | null;
|
?.split("=")[1];
|
||||||
iat: number;
|
|
||||||
exp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProtectedRoutes = ({ allowedRoles }: RouteProps) => {
|
|
||||||
const token = sessionStorage.getItem("token");
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return <Navigate to="/login-internify" replace />;
|
return <Navigate to="/login-internify" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decoded = decodeJWT<JwtPayload>(token);
|
const decoded = decodeJWT(token);
|
||||||
const userRole = decoded?.role as Role | undefined;
|
const userRole = decoded?.role as "intern" | "admin" | "mentor" | undefined;
|
||||||
|
|
||||||
if (!userRole) {
|
if (!userRole) {
|
||||||
sessionStorage.removeItem("token");
|
|
||||||
return <Navigate to="/login-internify" replace />;
|
return <Navigate to="/login-internify" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowedRoles && !allowedRoles.includes(userRole)) {
|
if (allowedRoles && !allowedRoles.includes(userRole)) {
|
||||||
return (
|
if (userRole === "admin" || userRole === "mentor") return <Navigate to="/mentor" replace />;
|
||||||
<Navigate
|
if (userRole === "intern") return <Navigate to="/internship" replace />;
|
||||||
to={userRole === "intern" ? "/intern" : "/mentor"}
|
|
||||||
replace
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
|
|||||||
+5
-3
@@ -1,18 +1,20 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string;
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const token = sessionStorage.getItem("token");
|
const token = document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("token="))
|
||||||
|
?.split("=")[1];
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user