diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx index e4be35c..9e860b5 100644 --- a/src/Pages/internify/LoginInternify.tsx +++ b/src/Pages/internify/LoginInternify.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { decodeJWT } from "../utils/decodeJWT"; import logo from "../../assets/logo.png"; import { useNavigate } from "react-router-dom"; import { Mail, Lock, Eye, EyeOff } from 'lucide-react'; @@ -6,15 +7,14 @@ import { useLogin } from "../../hooks/useUser"; import { customToast } from "../utils/showToast"; import { CircleQuestionMark } from 'lucide-react'; -const decodeJWT = (token: string) => { - try { - const base64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"); - const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "="); - return JSON.parse(atob(padded)); - } catch { - return null; - } -}; +interface JwtPayload { + id: string; + email: string; + role: "intern" | "mentor" | "admin"; + signature?: string | null; + iat: number; + exp: number; +} export default function InternifyLogin() { const navigate = useNavigate(); @@ -40,18 +40,28 @@ export default function InternifyLogin() { } const data = await login(email, password); - if (!data) return; - const { token } = data; - document.cookie = `token=${token}; path=/; SameSite=Strict`; + if (!data?.token) { + setError("Token login tidak ditemukan."); + return; + } - const decoded = decodeJWT(token); + const decoded = decodeJWT(data.token); const role = decoded?.role; + + if (!role) { + setError("Token login tidak valid."); + return; + } + + sessionStorage.setItem("token", data.token); + if (role === "intern") { - navigate("/intern"); + navigate("/intern", { replace: true }); } else if (role === "mentor" || role === "admin") { - navigate("/mentor"); + navigate("/mentor", { replace: true }); } else { + sessionStorage.removeItem("token"); setError("Role user tidak dikenali."); } }; diff --git a/src/Pages/internify/intern/components/SubmissionStatusTable.tsx b/src/Pages/internify/intern/components/SubmissionStatusTable.tsx index 88d73c1..57d42e6 100644 --- a/src/Pages/internify/intern/components/SubmissionStatusTable.tsx +++ b/src/Pages/internify/intern/components/SubmissionStatusTable.tsx @@ -15,7 +15,7 @@ interface SubmitStatusTableProps { function getTimeEarly(submittedAt: Date, deadline: Date): string { const diffMs = deadline.getTime() - submittedAt.getTime(); - if (diffMs <= 0) return "Submitted after deadline"; + if (diffMs <= 0) return `Submitted after deadline (${Math.abs(Math.floor(diffMs / (1000 * 60 * 60)))} hours late)`; const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const days = Math.floor(diffHours / 24); const hours = diffHours % 24; diff --git a/src/Pages/utils/ProtectedRoute.tsx b/src/Pages/utils/ProtectedRoute.tsx index 7466ab9..084f6af 100644 --- a/src/Pages/utils/ProtectedRoute.tsx +++ b/src/Pages/utils/ProtectedRoute.tsx @@ -1,46 +1,42 @@ import { Outlet, Navigate } from "react-router-dom"; +import { decodeJWT } from "./decodeJWT"; -interface PrivateRouteProps { - allowedRoles?: ("intern" | "admin" | "mentor")[]; +type Role = "intern" | "admin" | "mentor"; +interface RouteProps { + allowedRoles?: Role[]; } -const decodeJWT = (token: string) => { - try { - const base64Url = token.split(".")[1]; - const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); - const jsonPayload = decodeURIComponent( - window - .atob(base64) - .split("") - .map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)) - .join("") - ); - return JSON.parse(jsonPayload); - } catch (error) { - return null; - } -}; +interface JwtPayload { + id: string; + email: string; + role: "intern" | "mentor" | "admin"; + signature?: string | null; + iat: number; + exp: number; +} -const ProtectedRoutes = ({ allowedRoles }: PrivateRouteProps) => { - const token = document.cookie - .split("; ") - .find((row) => row.startsWith("token=")) - ?.split("=")[1]; +const ProtectedRoutes = ({ allowedRoles }: RouteProps) => { + const token = sessionStorage.getItem("token"); if (!token) { return ; } - const decoded = decodeJWT(token); - const userRole = decoded?.role as "intern" | "admin" | "mentor" | undefined; + const decoded = decodeJWT(token); + const userRole = decoded?.role as Role | undefined; if (!userRole) { + sessionStorage.removeItem("token"); return ; } if (allowedRoles && !allowedRoles.includes(userRole)) { - if (userRole === "admin" || userRole === "mentor") return ; - if (userRole === "intern") return ; + return ( + + ); } return ; diff --git a/src/Pages/utils/decodeJWT.ts b/src/Pages/utils/decodeJWT.ts new file mode 100644 index 0000000..48d94a5 --- /dev/null +++ b/src/Pages/utils/decodeJWT.ts @@ -0,0 +1,20 @@ +export const decodeJWT = (token: string): T | null => { + try { + const payload = token.split(".")[1]; + if (!payload) return null; + + const base64 = payload + .replace(/-/g, "+") + .replace(/_/g, "/"); + + const padded = base64.padEnd( + base64.length + ((4 - (base64.length % 4)) % 4), + "=" + ); + + return JSON.parse(window.atob(padded)); + } catch (error) { + console.error("Gagal decode JWT:", error); + return null; + } +}; \ No newline at end of file diff --git a/src/hooks/useTasks.ts b/src/hooks/useTasks.ts index a74d994..64f8609 100644 --- a/src/hooks/useTasks.ts +++ b/src/hooks/useTasks.ts @@ -6,6 +6,7 @@ import type { TaskSubmissionData, AdminTaskDetail, MentorTaskItem, + ProjectTask, } from "../types/task.types"; import { getProjectTasks, @@ -24,6 +25,7 @@ import { import { useCurrentUser } from "./useUser"; import { useMyTasks } from "./useProjects"; import type { InternTaskItem } from "../types/project.types"; +import { getProjects, getProjectById } from "../services/ProjectService"; // GET /task-api/projects/{id_project}/tasks export const useProjectTasks = (projectId: number) => { @@ -267,7 +269,7 @@ export const useSubmission = ( }; }; -// hooks/useAllMentorTasks.ts +// hooks/useAllMentorTasks.ts - Ambil semua tasks untuk mentor (tanpa filter project)/hanya ambil tasks yang dibuat pengguna tersebut (mentor) untuk semua project yang dia ikuti. Ini digunakan untuk menampilkan semua tasks mentor di halaman dashboard mentor. export const useAllMentorTasks = (enabled: boolean) => { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(false); @@ -290,36 +292,97 @@ export interface Deadline { label: string; } +// hooks.useRetrieveAllTasks.ts - Ambil semua tasks untuk admin tanpa adanya batasan dari pembuat task (mentor), bertujuan agar admin bisa melihat semua task yang ada di dalam database +interface AdminTask extends ProjectTask { + project_name: string; +} +export const useRetrieveAllTasks = (enabled = true) => { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!enabled) return; + + let isMounted = true; + + const fetchAllTasks = async () => { + setLoading(true); + setError(null); + + try { + const projects = await getProjects(); + const projectDetails = await Promise.all( + projects.map((project) => getProjectById(project.id)), + ); + + if (!isMounted) return; + + const allTasks = projectDetails.flatMap((project) => + project.tasks.map((task) => ({ + ...task, + project_name: project.project_name, + })), + ); + setTasks(allTasks); + } catch (err) { + if (!isMounted) return; + setError("Gagal memuat semua tasks."); + } finally { + if (!isMounted) return; + setLoading(false); + } + }; + + fetchAllTasks(); + return () => { + isMounted = false; + }; + }, [enabled]); + + return { tasks, loading, error }; +} + export const useDeadlines = () => { const { user, loading: userLoading, error: userError } = useCurrentUser(); const isIntern = user?.role === "intern"; + const isMentor = user?.role === "mentor"; + const isAdmin = user?.role === "admin"; - const { - tasks: internTasks, - loading: internLoading, - error: internError, - } = useMyTasks(); - const { - tasks: mentorTasks, - loading: mentorLoading, - error: mentorError, - } = useAllMentorTasks(!isIntern && user !== null); + const { tasks: internTasks, loading: internLoading, error: internError } = useMyTasks(); + const { tasks: mentorTasks, loading: mentorLoading, error: mentorError } = useAllMentorTasks(isMentor); + const { tasks: allTasks, loading: allTasksLoading, error: allTasksError } = useRetrieveAllTasks(isAdmin); const deadlines: Deadline[] = useMemo(() => { if (!user) return []; - return isIntern - ? internTasks.map((task) => ({ + + if (isIntern) { + return internTasks.map((task) => ({ date: new Date(task.deadline_at), label: task.title, - })) - : mentorTasks.map((task) => ({ + })); + } + + if (isMentor) { + return mentorTasks.map((task) => ({ date: new Date(task.deadline_at), label: `[${task.project_name}] ${task.title}`, })); - }, [isIntern, internTasks, mentorTasks, user]); - const loading = userLoading || (isIntern ? internLoading : mentorLoading); - const error = userError || (isIntern ? internError : mentorError); + } + + if (isAdmin) { + return allTasks.map((task) => ({ + date: new Date(task.deadline_at), + label: `[${task.project_name}] ${task.title}`, + })); + } + + return []; + }, [isAdmin, isIntern, isMentor, allTasks, internTasks, mentorTasks, user]); + + const loading = userLoading || (isIntern ? internLoading : isMentor ? mentorLoading : allTasksLoading); + const error = userError || (isIntern ? internError : isMentor ? mentorError : allTasksError); return { deadlines, loading, error, isIntern }; }; diff --git a/src/lib/api.ts b/src/lib/api.ts index 83150f3..fb099f1 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,20 +1,18 @@ import axios from "axios"; -export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string; +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; const api = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, }); api.interceptors.request.use((config) => { - const token = document.cookie - .split("; ") - .find((row) => row.startsWith("token=")) - ?.split("=")[1]; + const token = sessionStorage.getItem("token"); if (token) { config.headers.Authorization = `Bearer ${token}`; } + return config; }); diff --git a/src/services/ProjectService.ts b/src/services/ProjectService.ts index 14790f7..7ed6fc1 100644 --- a/src/services/ProjectService.ts +++ b/src/services/ProjectService.ts @@ -63,6 +63,17 @@ export const getProjectById = async ( position: m.user?.position, kelompok_peminatan: m.user?.kelompok_peminatan, })), + tasks: raw.tasks.map((t: any) => ({ + id: t.id, + id_project: raw.id, + slug: t.slug, + title: t.title, + description: t.description, + deadline_at: t.deadline_at, + submission_type: t.submission_type, + created_at: t.created_at, + updated_at: t.updated_at, + })), template_sertificate: raw.template_sertificate, }; };