addons: implement JWT decoding utility and enhance role-based navigation in ProtectedRoute; fix deadlines task data on admin-role user;
This commit is contained in:
@@ -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<JwtPayload>(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.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <Navigate to="/login-internify" replace />;
|
||||
}
|
||||
|
||||
const decoded = decodeJWT(token);
|
||||
const userRole = decoded?.role as "intern" | "admin" | "mentor" | undefined;
|
||||
const decoded = decodeJWT<JwtPayload>(token);
|
||||
const userRole = decoded?.role as Role | undefined;
|
||||
|
||||
if (!userRole) {
|
||||
sessionStorage.removeItem("token");
|
||||
return <Navigate to="/login-internify" replace />;
|
||||
}
|
||||
|
||||
if (allowedRoles && !allowedRoles.includes(userRole)) {
|
||||
if (userRole === "admin" || userRole === "mentor") return <Navigate to="/mentor" replace />;
|
||||
if (userRole === "intern") return <Navigate to="/intern" replace />;
|
||||
return (
|
||||
<Navigate
|
||||
to={userRole === "intern" ? "/intern" : "/mentor"}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export const decodeJWT = <T = any>(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;
|
||||
}
|
||||
};
|
||||
+81
-18
@@ -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<MentorTaskItem[]>([]);
|
||||
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<AdminTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 };
|
||||
};
|
||||
|
||||
+3
-5
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user