Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/admin #139

Merged
merged 4 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/uploadStage/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const Button: React.FC<ButtonProps> = ({id, labelText, onClick, className = "",
<button
id={id}
onClick={onClick}
className={`block min-h-4 p-2 text-sm font-medium text-white bg-blue-500 rounded-lg border border-blue-500 hover:bg-blue-600 focus:outline-2 focus:outline-blue-600 ${className}`}
className={`block w-full min-h-4 p-2 text-sm text-white bg-blue-500 rounded-lg border border-blue-500 hover:bg-blue-600 focus:outline-2 focus:outline-blue-600 ${className}`}
disabled={disabled}
>
{labelText}
Expand Down
2 changes: 1 addition & 1 deletion src/components/uploadStage/Textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Textarea: React.FC<TextAreaProps> = ({labelText, message, onChange, placeh
}) => {
return (
<div className="w-4/5 m-4 space-y-2 flex flex-1 flex-col">
<label htmlFor="message" className="block ml-1 text-sm font-medium text-gray-900">
<label htmlFor="message" className="block ml-1 text-sm font-bold text-gray-900">
{labelText}
</label>
<textarea
Expand Down
112 changes: 76 additions & 36 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import {
} from "next/dist/compiled/@edge-runtime/cookies";
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import {notFound} from "next/navigation";

import {isAdmin} from "@/utils/protection/protectAdminAccess";

const url = process.env.NEXT_PUBLIC_BASE_API;
const protectedPaths = ["/problem", "/challengelist"];
const playerAccessiblePaths = ["/problem", "/challengelist"];
const adminAccessiblePaths = ["/admin"];

const ReAccessToken = async (refreshToken: string) => {
try {
Expand All @@ -28,41 +32,24 @@ const ReAccessToken = async (refreshToken: string) => {
}
}


export async function middleware(request: NextRequest) {
const { nextUrl, cookies } = request;
const { pathname, host } = nextUrl;
const accessToken = cookies.get("POL_ACCESS_TOKEN");
const refreshToken = cookies.get("POL_REFRESH_TOKEN");
if (accessToken === undefined && refreshToken !== undefined) {
if (!accessToken) {
const token = await ReAccessToken(refreshToken.value)
if (token) {
const now = new Date();
const time = now.getTime();
const response = NextResponse.redirect(request.url);
response.cookies.set({
name: "POL_ACCESS_TOKEN",
value: token,
domain: "",
expires: time + 1000 * 60 * 60
});
applySetCookie(request, response);
return response;
}
}
}
if (protectedPaths.some(path => pathname.startsWith(path)) && accessToken === undefined) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
async function refreshAccessToken(request: NextRequest, refreshToken: string): Promise<NextResponse | null> {
const token = await ReAccessToken(refreshToken);
if (token) {
const now = new Date();
const time = now.getTime();
const response = NextResponse.redirect(request.url);
response.cookies.set({
name: "POL_ACCESS_TOKEN",
value: token,
domain: "",
expires: time + 1000 * 60 * 60
});
applySetCookie(request, response);
return response;
}
return null;
}

export const config = {
matcher: '/:path*',
};

function applySetCookie(req: NextRequest, res: NextResponse): void {
const resCookies = new ResponseCookies(res.headers);
const newReqHeaders = new Headers(req.headers);
Expand All @@ -74,10 +61,63 @@ function applySetCookie(req: NextRequest, res: NextResponse): void {
request: { headers: newReqHeaders },
}).headers.forEach((value, key) => {
if (
key === "x-middleware-override-headers" ||
key.startsWith("x-middleware-request-")
key === "x-middleware-override-headers" ||
key.startsWith("x-middleware-request-")
) {
res.headers.set(key, value);
}
});
}
}

const isPlayerAccessiblePath = (pathname: string): boolean => {
return playerAccessiblePaths.some(path => pathname.startsWith(path));
}

const redirectToLogin = (request: NextRequest, pathname: string): NextResponse => {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}

const isAdminAccessiblePath = (pathname: string): boolean => {
return adminAccessiblePaths.some(path => pathname.startsWith(path));
}

const redirectToNotFound = (request: NextRequest): NextResponse => {
const url = new URL('/not-found', request.url);
return NextResponse.rewrite(url);
}

export async function middleware(request: NextRequest) {
const { nextUrl, cookies } = request;
const { pathname, host } = nextUrl;
const accessToken = cookies.get("POL_ACCESS_TOKEN");
const refreshToken = cookies.get("POL_REFRESH_TOKEN");

if (!accessToken && refreshToken) {
const response = await refreshAccessToken(request, refreshToken.value);
if (response) {
return response;
}
}

if (isPlayerAccessiblePath(pathname) && !accessToken) {
return redirectToLogin(request, pathname);
}

if (isAdminAccessiblePath(pathname)) {
if (!accessToken) {
return redirectToLogin(request, pathname);
}
const isAdminPlayer = await isAdmin(accessToken.value);
if (!isAdminPlayer) {
return redirectToNotFound(request);
}
}

return NextResponse.next();
}

export const config = {
matcher: '/:path*',
};
5 changes: 5 additions & 0 deletions src/types/basicResponseDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface BasicResponseDto {
"response": string
"error": string,
"detail": string
}
23 changes: 23 additions & 0 deletions src/utils/protection/protectAdminAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {BasicResponseDto} from "@/types/basicResponseDto";

const url = process.env.NEXT_PUBLIC_BASE_API;
export const isAdmin = async (accessToken: string) => {
try {
const response = await fetch(`${url}/api/players/me/admin`, {
credentials: "include",
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

if (!response.ok) {
return false;
}

const body: BasicResponseDto = await response.json();

return body.response === "true";
} catch (error) {
console.log(error);
}
}