Skip to content

Commit

Permalink
Merge pull request #82 from CS3219-AY2425S1/bug-fix
Browse files Browse the repository at this point in the history
Bug fix for route protection
  • Loading branch information
mounilsankar authored Oct 20, 2024
2 parents c9df877 + 4579b4c commit 873fb6c
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 18 deletions.
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
- "3000:3000"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_API_URL=http://nginx
depends_on:
- user-service
- question-service
Expand Down
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ yarn-error.log*

# local env files
.env*.local
.env.docker

# vercel
.vercel
Expand Down
11 changes: 11 additions & 0 deletions frontend/context/UserContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createContext, useState, ReactNode, useEffect } from "react";

import { User } from "@/types/user";
import { useVerifyToken } from "@/hooks/api/auth";

interface UserContextProps {
user: User | null;
Expand All @@ -12,6 +13,16 @@ export const UserContext = createContext<UserContextProps | null>(null);
export const UserProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);

const { data, error } = useVerifyToken();

useEffect(() => {
if (data) {
setUser(data);
} else if (error) {
setUser(null);
}
}, [data, error]);

return (
<UserContext.Provider value={{ user, setUser }}>
{children}
Expand Down
16 changes: 15 additions & 1 deletion frontend/hooks/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { jwtDecode } from "jwt-decode";

Expand All @@ -10,6 +10,7 @@ import {
RegisterCredentials,
LogoutResponse,
} from "@/types/auth";
import { User } from "@/types/user";

// Login: Send credentials to the server and return the response
const login = async (credentials: Credentials): Promise<LoginResponse> => {
Expand Down Expand Up @@ -177,3 +178,16 @@ export const useLogout = () => {
},
});
};

const verifyToken = async () => {
const response = await axios.get(`/user-service/auth/verify-token`);

return response.data.data;
};

export const useVerifyToken = () => {
return useQuery<User, AxiosError>({
queryKey: ["verifyToken"],
queryFn: () => verifyToken(),
});
};
75 changes: 58 additions & 17 deletions frontend/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,77 @@ import type { NextRequest } from "next/server";

import { NextResponse } from "next/server";

// Define public routes that don't require authentication
const publicRoutes = [
"/",
"/login",
"/register",
"/forget-password",
"/reset-password",
];

const baseURL = process.env.NEXT_PUBLIC_API_URL;

const validateToken = async (accessToken: string | undefined): Promise<any> => {
try {
const response = await fetch(
`${baseURL}/api/user-service/auth/verify-token`,
{
method: "GET",
headers: {
Cookie: `accessToken=${accessToken}`,
"Content-Type": "application/json",
},
},
);

if (!response.ok) {
throw new Error("Token validation failed");
}

return await response.json();
} catch (error) {
console.error("Unexpected error:", error);
}
};

export async function middleware(req: NextRequest) {
const url = req.nextUrl;
const accessToken = req.cookies.get("accessToken")?.value;
const refreshToken = req.cookies.get("refreshToken")?.value;

// Define public routes that don't require authentication
const publicRoutes = [
"/",
"/login",
"/register",
"/forget-password",
"/reset-password",
];

// Allow access to public routes without checking tokens
if (publicRoutes.some((route) => url.pathname === route)) {
return NextResponse.next();
}

// If both access token and refresh token are present, allow the user to proceed
if (accessToken && refreshToken) {
// If the access token is present, validate it
try {
const decodedAccessToken = await validateToken(accessToken);

if (!decodedAccessToken) {
console.log("Invalid access token, redirecting to login");

return NextResponse.redirect(new URL("/login", req.nextUrl));
}

// Extract user role from the decoded token (assuming the role is stored in the token)
const isAdmin = decodedAccessToken.data.isAdmin;

// Check if the user is trying to access an admin route (all routes starting with /admin)
if (url.pathname.startsWith("/admin") && !isAdmin) {
console.log("User is not an admin, redirecting to 403 page");

return NextResponse.redirect(new URL("/403", req.nextUrl)); // 403 Forbidden
}

console.log("User is authenticated");

return NextResponse.next();
}
} catch (error) {
console.error("Token validation failed, redirecting to login:", error);

// If no access token and the user is not on a public route, redirect to login
if (!accessToken && !refreshToken) {
return NextResponse.redirect(new URL("/login", req.nextUrl));
}

return NextResponse.next();
}

// Apply middleware to specific paths
Expand Down
17 changes: 17 additions & 0 deletions frontend/pages/403/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client";

import DefaultLayout from "@/layouts/default";

const The403Page = () => {
return (
<>
<DefaultLayout isLoggedIn={true}>
<div className="flex justify-center">
<p>403 Forbidden Access</p>
</div>
</DefaultLayout>
</>
);
};

export default The403Page;

0 comments on commit 873fb6c

Please sign in to comment.