Skip to content

Commit

Permalink
refactor(pages): enhance authorization handling, improve NotAuthorize…
Browse files Browse the repository at this point in the history
…dPage
  • Loading branch information
loklokyx committed Feb 16, 2025
1 parent a38452a commit 739a04d
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 61 deletions.
88 changes: 71 additions & 17 deletions client/src/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";

import Navbar from "@/components/navbar";
import Sidebar from "@/components/sidebar";
import { Button } from "@/components/ui/button";
import Footer from "@/components/ui/footer";
import { WaitingLoader } from "@/components/ui/loading";
import { useAuth } from "@/context/auth-provider";
Expand All @@ -15,34 +17,58 @@ interface ProtectedPageProps {

export function ProtectedPage({ children, requiredRoles }: ProtectedPageProps) {
const { userRole, isLoggedIn } = useAuth();
const router = useRouter();
const [isInitializing, setIsInitializing] = useState(true);
const [authState, setAuthState] = useState<
"initializing" | "authorized" | "unauthorized" | "wrong-role"
>("initializing");

useEffect(() => {
// Wait for one tick to ensure auth state is properly initialized
const timer = setTimeout(() => {
setIsInitializing(false);
if (!isLoggedIn || !requiredRoles.includes(userRole)) {
router.replace(`/not-authorized?next=${router.pathname}`);
if (!isLoggedIn || !userRole) {
setAuthState("unauthorized");
} else if (!requiredRoles.includes(userRole)) {
setAuthState("wrong-role");
} else {
setAuthState("authorized");
}
}, 0);

return () => clearTimeout(timer);
}, [isLoggedIn, userRole, requiredRoles, router]);
}, [isLoggedIn, userRole, requiredRoles]);

if (isInitializing) return <WaitingLoader />;
if (!isLoggedIn || !requiredRoles.includes(userRole))
return <WaitingLoader />;

// Render authorized content
return (
<Sidebar
role={userRole.toLowerCase() as Role}
isShowBreadcrumb={userRole !== Role.STUDENT}
>
{children}
</Sidebar>
);
console.log(userRole);
switch (authState) {
case "unauthorized":
return (
<PublicPage>
<NotAuthorizedPage />
</PublicPage>
);
case "wrong-role":
return (
<Sidebar
role={userRole.toLowerCase() as Role}
isShowBreadcrumb={userRole !== Role.STUDENT}
>
<PublicPage isNavBar={false} isFooter={false}>
<NotAuthorizedPage />
</PublicPage>
</Sidebar>
);
case "authorized":
return (
<Sidebar
role={userRole.toLowerCase() as Role}
isShowBreadcrumb={userRole !== Role.STUDENT}
>
{children}
</Sidebar>
);
default:
return <WaitingLoader />;
}
}

interface PublicPageProps {
Expand All @@ -64,3 +90,31 @@ export function PublicPage({
</div>
);
}

function NotAuthorizedPage() {
const router = useRouter();
const { userRole, logout } = useAuth();

const handleLogout = () => {
router.push("/").then(() => logout());
};

return (
<div className="animate-fade-in flex min-h-[50vh] flex-col items-center justify-center gap-5 py-4 text-center">
<h1 className="font-black text-red-600">Access Denied</h1>
<p className="text-lg font-bold text-gray-600">
{userRole
? `Role[${userRole}] do not have permission to view this page.`
: `Unabled to identify user.`}
</p>
<Button
onClick={userRole ? () => router.push("/dashboard") : handleLogout}
variant="outline"
className="mt-6 flex animate-bounce items-center gap-2"
>
<ArrowLeft size={18} />
{userRole ? "Back to Dashboard" : "Logout"}
</Button>
</div>
);
}
5 changes: 3 additions & 2 deletions client/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ api.interceptors.response.use(

const refreshTokenValid =
tokenState.refresh != undefined && tokenState.refresh.expiry > Date.now();
const isAuthError =
error.response?.status === 401 || error.response?.status === 403;
// const isAuthError =
// error.response?.status === 401 || error.response?.status === 403;
const isAuthError = error.response?.status === 401;

if (
refreshTokenValid == true &&
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function PageConfig() {
function Dashboard() {
const userRole = Cookies.get("user_role");
return (
<div className="flex h-[90vh] items-center justify-center">
<div className="container flex h-[90vh] items-center justify-center text-balance">
<h1>
Welcome to{" "}
{userRole && `${userRole.charAt(0).toUpperCase()}${userRole.slice(1)}`}{" "}
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/dashboard/users/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ProtectedPage } from "@/components/layout";
import { Role } from "@/types/user";

export default function PageConfig() {
const roles = [Role.ADMIN, Role.TEACHER];
const roles = [Role.ADMIN];
return (
<ProtectedPage requiredRoles={roles}>
<Create />
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/dashboard/users/teachers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { Teacher } from "@/types/user";
import { Role } from "@/types/user";

export default function PageConfig() {
const roles = [Role.ADMIN, Role.TEACHER];
const roles = [Role.ADMIN];
return (
<ProtectedPage requiredRoles={roles}>
<Teachers />
Expand Down
74 changes: 38 additions & 36 deletions client/src/pages/not-authorized.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,52 @@ import { PublicPage } from "@/components/layout";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/context/auth-provider";

export default function PageConfig() {
return (
<PublicPage>
<NotAuthorizedPage />
</PublicPage>
);
interface Props {
isNavBar?: boolean;
isFooter?: boolean;
nextPath?: string | undefined;
}

function NotAuthorizedPage() {
export default function NotAuthorizedPage({
isNavBar = true,
isFooter = true,
}: Props) {
const router = useRouter();
const { userRole, logout } = useAuth();

const handleLogout = () => {
logout();
router.push("/");
router.push("/").then(() => logout());
};

return (
<div className="animate-fade-in flex min-h-[50vh] flex-col items-center justify-center gap-5 py-4 text-center">
<h1 className="font-black text-red-600">Access Denied</h1>
<p className="text-lg font-bold text-gray-600">
{userRole
? `${userRole} do not have permission to view this page.`
: `Unabled to get Role.`}
</p>
{userRole ? (
<Button
onClick={() => router.push("/")}
variant="outline"
className="mt-6 flex animate-bounce items-center gap-2"
>
<ArrowLeft size={18} />
Back to Home
</Button>
) : (
<Button
onClick={handleLogout}
variant="outline"
className="mt-6 flex animate-bounce items-center gap-2"
>
<ArrowLeft size={18} />
Logout
</Button>
)}
</div>
<PublicPage isNavBar={isNavBar} isFooter={isFooter}>
<div className="animate-fade-in flex min-h-[50vh] flex-col items-center justify-center gap-5 py-4 text-center">
<h1 className="font-black text-red-600">Access Denied</h1>
<p className="text-lg font-bold text-gray-600">
{userRole
? `${userRole} do not have permission to view this page.`
: `Unabled to get Role.`}
</p>
{userRole ? (
<Button
onClick={() => router.push("/dashboard")}
variant="outline"
className="mt-6 flex animate-bounce items-center gap-2"
>
<ArrowLeft size={18} />
Back to Home
</Button>
) : (
<Button
onClick={handleLogout}
variant="outline"
className="mt-6 flex animate-bounce items-center gap-2"
>
<ArrowLeft size={18} />
Logout
</Button>
)}
</div>
</PublicPage>
);
}
2 changes: 1 addition & 1 deletion server/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class SchoolViewSet(viewsets.ModelViewSet):
queryset = School.objects.all()
serializer_class = SchoolSerializer
permission_classes = [IsAuthenticated | IsStudent | IsTeacher | IsAdmin]
permission_classes = [IsAuthenticated or IsStudent or IsTeacher or IsAdmin]
'''
Expand Down
2 changes: 1 addition & 1 deletion server/api/team/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .serializers import TeamSerializer, TeamMemberSerializer


@permission_classes([IsTeacher | IsAdmin | IsAdminUser])
@permission_classes([IsTeacher or IsAdmin or IsAdminUser])
class TeamViewSet(viewsets.ModelViewSet):
serializer_class = TeamSerializer
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
Expand Down
2 changes: 1 addition & 1 deletion server/api/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def destroy(self, request, *args, **kwargs):
{"error": "You do not have permission to access this resource."}, status=status.HTTP_403_FORBIDDEN)


@permission_classes([IsTeacher | IsAdmin | IsAdminUser])
@permission_classes([IsTeacher or IsAdmin or IsAdminUser])
class SchoolViewSet(viewsets.ModelViewSet):
"""
A viewset for managing schools.
Expand Down

0 comments on commit 739a04d

Please sign in to comment.