feat: update intern routes and navigation to use '/internship' path; refactor token handling in API and authentication components

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-08-03 14:42:22 +07:00
parent 252671a2cc
commit 5dbb21bf5a
9 changed files with 44 additions and 73 deletions
+1 -1
View File
@@ -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 />} />
+10 -10
View File
@@ -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;
+7 -26
View File
@@ -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.");
} }
}; };
+1 -1
View File
@@ -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 });
+1 -1
View File
@@ -10,7 +10,7 @@ const PrivateRoute = () => {
useEffect(() => { useEffect(() => {
if (!token) { if (!token) {
navigate("/humic-20242025-2"); navigate("/login-admin");
} }
}, [navigate, token]); }, [navigate, token]);
+12 -24
View File
@@ -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
View File
@@ -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;
}); });