From 6213c3f33d2f2089867c42d9e67769e52cfb41a4 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 18 Feb 2024 14:59:54 -0500 Subject: [PATCH 01/28] add authLevel route --- next/app/api/authLevel/route.ts | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 next/app/api/authLevel/route.ts diff --git a/next/app/api/authLevel/route.ts b/next/app/api/authLevel/route.ts new file mode 100644 index 00000000..150a2f4e --- /dev/null +++ b/next/app/api/authLevel/route.ts @@ -0,0 +1,49 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +/** + * HTTP GET request to /api/authLevel/ + * "None" => user does not exist + * "User" => user exists but has no other credentials + * "Member" => user is an SSE member but has no other credentials + * "Mentor" => user is a mentor but not an officer + * "Officer" => user is an officer + * "Mega Rayquaza EX" => I am Burnt Out ;-; + * @param request {email: string} + * @returns {"None" | "User" | "Member" | "Mentor" | "Officer" | "Mega Rayquaza EX"} the auth level + */ +export async function GET(request: Request) { + let body; + try { + body = await request.json(); + } catch { + return new Response("Invalid JSON", { status: 422 }); + } + + const user = await prisma.user.findFirst({ + where: { + email: body.email, + }, + select: { + mentor: true, + officers: true, + isMember: true, + }, + }); + if (user == null) { + return new Response("None"); + } + // deconstruct the user object + const { mentor, officers, isMember } = user; + if (officers.length > 0) { + return new Response("Officer"); + } + if (mentor.length > 0) { + return new Response("Mentor"); + } + if (isMember) { + return new Response("Member"); + } + return new Response("User"); +} From 3bb675644aadc4e691a40f3458fd416ae7b54ad9 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 18 Feb 2024 15:26:33 -0500 Subject: [PATCH 02/28] initial auth middleware --- next/app/api/authLevel/route.ts | 2 +- next/lib/middlewares/authentication.ts | 44 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 next/lib/middlewares/authentication.ts diff --git a/next/app/api/authLevel/route.ts b/next/app/api/authLevel/route.ts index 150a2f4e..f0af60fa 100644 --- a/next/app/api/authLevel/route.ts +++ b/next/app/api/authLevel/route.ts @@ -21,7 +21,7 @@ export async function GET(request: Request) { return new Response("Invalid JSON", { status: 422 }); } - const user = await prisma.user.findFirst({ + const user = await prisma.user.findUnique({ where: { email: body.email, }, diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts new file mode 100644 index 00000000..48960ca7 --- /dev/null +++ b/next/lib/middlewares/authentication.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * check the URL to see what level of authorization is required + * @param route the API route to check + * @returns {"None" | "User" | "Member" | "Mentor" | "Officer"} + */ +const routeAuthType = (method: string, route: string) => { + if ( + // these should only ever be accessible to officers + route.startsWith("/api/golinks/officer/") || + // these should only be modified by officers + ((route.startsWith("/api/hourBlocks/") || + route.startsWith("/api/departments/") || + route.startsWith("/api/officer/") || + route.startsWith("/api/skill/") || + route.startsWith("/api/course/")) && + method != "GET") + ) { + return "Officer"; + } + if ( + // these should only be modified by mentors + (route.startsWith("/api/schedule/") || + route.startsWith("/api/mentorSkill/") || + route.startsWith("/api/mentor/") || + route.startsWith("/api/courseTaken/")) && + method != "GET" + ) { + return "Mentor"; + } + return "None"; +}; + +export const authMiddleware = async (request: NextRequest) => { + const { pathname } = request.nextUrl; + const method = request.method; + + const headers = request.headers; + + if (routeAuthType(method, pathname) == "None") { + return NextResponse.next(); + } +}; From bf6a18e1c4e6de9288d25692094389f8e275fb79 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 25 Feb 2024 17:00:07 -0500 Subject: [PATCH 03/28] Add authentication middleware proper, fix handling of multiple middlewares --- next/app/api/authLevel/route.ts | 58 +++++++++++++++---------- next/lib/middlewares/authentication.ts | 59 ++++++++++++++++++++------ next/middleware.ts | 29 ++++++++----- 3 files changed, 102 insertions(+), 44 deletions(-) diff --git a/next/app/api/authLevel/route.ts b/next/app/api/authLevel/route.ts index f0af60fa..7cc7cd25 100644 --- a/next/app/api/authLevel/route.ts +++ b/next/app/api/authLevel/route.ts @@ -9,11 +9,10 @@ const prisma = new PrismaClient(); * "Member" => user is an SSE member but has no other credentials * "Mentor" => user is a mentor but not an officer * "Officer" => user is an officer - * "Mega Rayquaza EX" => I am Burnt Out ;-; - * @param request {email: string} - * @returns {"None" | "User" | "Member" | "Mentor" | "Officer" | "Mega Rayquaza EX"} the auth level + * @param request {email: string} | {token: string} + * @returns {"None" | "User" | "Member" | "Mentor" | "Officer"} the auth level */ -export async function GET(request: Request) { +export async function PUT(request: Request) { let body; try { body = await request.json(); @@ -21,29 +20,44 @@ export async function GET(request: Request) { return new Response("Invalid JSON", { status: 422 }); } - const user = await prisma.user.findUnique({ - where: { - email: body.email, - }, + const user = await prisma.user.findFirst({ + where: + "email" in body + ? { + email: body.email, + } + : { + session: { + some: { + sessionToken: body.token, + }, + }, + }, select: { mentor: true, officers: true, isMember: true, }, }); - if (user == null) { - return new Response("None"); - } - // deconstruct the user object - const { mentor, officers, isMember } = user; - if (officers.length > 0) { - return new Response("Officer"); - } - if (mentor.length > 0) { - return new Response("Mentor"); - } - if (isMember) { - return new Response("Member"); + + const authLevel = { + isUser: false, + isMember: false, + isMentor: false, + isOfficer: false, + }; + + if (user != null) { + // deconstruct the user object + const { mentor, officers, isMember } = user; + if (officers.length > 0) { + authLevel.isOfficer = true; + } + if (mentor.length > 0) { + authLevel.isOfficer = true; + } + authLevel.isMember = isMember; + authLevel.isUser = true; } - return new Response("User"); + return Response.json(authLevel); } diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index 48960ca7..ad1e0c59 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -1,5 +1,8 @@ +import { PrismaClient } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; +const prisma = new PrismaClient(); + /** * check the URL to see what level of authorization is required * @param route the API route to check @@ -8,23 +11,23 @@ import { NextRequest, NextResponse } from "next/server"; const routeAuthType = (method: string, route: string) => { if ( // these should only ever be accessible to officers - route.startsWith("/api/golinks/officer/") || + route.startsWith("/api/golinks/officer") || // these should only be modified by officers - ((route.startsWith("/api/hourBlocks/") || - route.startsWith("/api/departments/") || - route.startsWith("/api/officer/") || - route.startsWith("/api/skill/") || - route.startsWith("/api/course/")) && + ((route.startsWith("/api/hourBlocks") || + route.startsWith("/api/departments") || + route.startsWith("/api/officer") || + route.startsWith("/api/skill") || + route.startsWith("/api/course")) && method != "GET") ) { return "Officer"; } if ( // these should only be modified by mentors - (route.startsWith("/api/schedule/") || - route.startsWith("/api/mentorSkill/") || - route.startsWith("/api/mentor/") || - route.startsWith("/api/courseTaken/")) && + (route.startsWith("/api/schedule") || + // route.startsWith("/api/mentorSkill") || + route.startsWith("/api/mentor") || + route.startsWith("/api/courseTaken")) && method != "GET" ) { return "Mentor"; @@ -33,12 +36,44 @@ const routeAuthType = (method: string, route: string) => { }; export const authMiddleware = async (request: NextRequest) => { + // console.log("Auth Middleware is running"); const { pathname } = request.nextUrl; const method = request.method; - const headers = request.headers; + // slice out the `Bearer ...` + const authToken = request.headers.get("Authorization")?.slice(7); + const authType = routeAuthType(method, pathname); + + if (authType == "None") { + // console.log("Letting through with no auth"); + return NextResponse.next(); + } + + const perm_fetch = await fetch(process.env.NEXTAUTH_URL + "/api/authLevel", { + body: JSON.stringify({ token: authToken }), + method: "PUT", + }); + // console.log(perm_fetch); + const permissions = await perm_fetch.json(); - if (routeAuthType(method, pathname) == "None") { + if ( + authType == "Mentor" && + // check if there is a mentor... + permissions.isMentor + ) { + // console.log("User is a Mentor"); + return NextResponse.next(); + } + if ( + authType == "Officer" && + // Check if there is an officer... + permissions.isOfficer + ) { + // console.log("User is an Officer"); return NextResponse.next(); } + // console.log("Access Denied"); + return new NextResponse(`Access Denied; need to be ${authType} to access`, { + status: 403, + }); }; diff --git a/next/middleware.ts b/next/middleware.ts index eac6d42f..561a8d1d 100644 --- a/next/middleware.ts +++ b/next/middleware.ts @@ -1,12 +1,21 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { golinksMiddleware } from './lib/middlewares/golinks'; +import { NextRequest, NextResponse } from "next/server"; +import { golinksMiddleware } from "./lib/middlewares/golinks"; +import { authMiddleware } from "./lib/middlewares/authentication"; export async function middleware(request: NextRequest) { - // Run the golinks middleware logic - let response = await golinksMiddleware(request); - // If the response is not NextResponse.next(), terminate the middleware chain - // and return the response. This would occur if the middleware redirects or rewrites. - if (response != NextResponse.next()) { - return response; - } -} \ No newline at end of file + console.log("Middleware is running on", request.url); + // Run the authentication middleware logic + let response = await authMiddleware(request); + // If the response is not NextResponse.next(), terminate the middleware chain + // and return the response. This would occur if the middleware redirects or rewrites. + if (response.headers.get("x-middleware-next") != "1") { + return response; + } + // Run the golinks middleware logic + response = await golinksMiddleware(request); + // If the response is not NextResponse.next(), terminate the middleware chain + // and return the response. This would occur if the middleware redirects or rewrites. + if (response.headers.get("x-middleware-next") != "1") { + return response; + } +} From 1cc502eea6c0cd59568f33f0e7671c1fe7528e7b Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 16:23:13 -0500 Subject: [PATCH 04/28] better db statement --- next/app/api/authLevel/route.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/next/app/api/authLevel/route.ts b/next/app/api/authLevel/route.ts index 7cc7cd25..3a789750 100644 --- a/next/app/api/authLevel/route.ts +++ b/next/app/api/authLevel/route.ts @@ -9,8 +9,8 @@ const prisma = new PrismaClient(); * "Member" => user is an SSE member but has no other credentials * "Mentor" => user is a mentor but not an officer * "Officer" => user is an officer - * @param request {email: string} | {token: string} - * @returns {"None" | "User" | "Member" | "Mentor" | "Officer"} the auth level + * @param request \{email: string} | {token: string} + * @returns \{isUser: boolean, isMember: boolean, isMentor: boolean, isOfficer: boolean} the auth level */ export async function PUT(request: Request) { let body; @@ -34,12 +34,22 @@ export async function PUT(request: Request) { }, }, select: { - mentor: true, - officers: true, + mentor: { + where: { + isActive: true, + }, + }, + officers: { + where: { + is_active: true, + }, + }, isMember: true, }, }); + // console.log("Getting Auth for ", body, user); + const authLevel = { isUser: false, isMember: false, @@ -54,7 +64,7 @@ export async function PUT(request: Request) { authLevel.isOfficer = true; } if (mentor.length > 0) { - authLevel.isOfficer = true; + authLevel.isMentor = true; } authLevel.isMember = isMember; authLevel.isUser = true; From 0078be1b4c808c7977a2015ec902bb201389a371 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 16:51:11 -0500 Subject: [PATCH 05/28] remove vulnerability where a user without an auth token would have officer-level access --- next/app/api/authLevel/route.ts | 56 ++++++++++++++++---------- next/lib/middlewares/authentication.ts | 17 ++++++-- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/next/app/api/authLevel/route.ts b/next/app/api/authLevel/route.ts index 3a789750..6037abc2 100644 --- a/next/app/api/authLevel/route.ts +++ b/next/app/api/authLevel/route.ts @@ -3,12 +3,20 @@ import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); /** - * HTTP GET request to /api/authLevel/ - * "None" => user does not exist - * "User" => user exists but has no other credentials - * "Member" => user is an SSE member but has no other credentials - * "Mentor" => user is a mentor but not an officer - * "Officer" => user is an officer + * HTTP PUT request to /api/authLevel/ + * Ideally this would be a GET request but the computer doesn't like it when I put data in the body of a GET. + * + * This route should be used to figure out what level of authorization a given user has. + * + * Either the user's email or session token should be provided in the request body. + * + * returns { + * * isUser: boolean -- whether the provided email/token corresponds to a valid user + * * isMember: boolean -- isUser && the user is a member + * * isMentor: boolean -- isUser && the user is an active mentor + * * isOfficer: boolean -- isUser && the user is an active officer + * + * } * @param request \{email: string} | {token: string} * @returns \{isUser: boolean, isMember: boolean, isMentor: boolean, isOfficer: boolean} the auth level */ @@ -20,6 +28,20 @@ export async function PUT(request: Request) { return new Response("Invalid JSON", { status: 422 }); } + // console.log("Getting Auth for ", body, user); + + const authLevel = { + isUser: false, + isMember: false, + isMentor: false, + isOfficer: false, + }; + + // if the email or token we get is null, don't call prisma + if (("email" in body ? body.email : body.token) == null) { + return Response.json(authLevel); + } + const user = await prisma.user.findFirst({ where: "email" in body @@ -34,29 +56,19 @@ export async function PUT(request: Request) { }, }, select: { + // select a minimal amount of data for active mentors mentor: { - where: { - isActive: true, - }, + where: { isActive: true }, + select: { id: true }, }, + // select a minimal amount of data for active officers officers: { - where: { - is_active: true, - }, + where: { is_active: true }, + select: { id: true }, }, isMember: true, }, }); - - // console.log("Getting Auth for ", body, user); - - const authLevel = { - isUser: false, - isMember: false, - isMentor: false, - isOfficer: false, - }; - if (user != null) { // deconstruct the user object const { mentor, officers, isMember } = user; diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index ad1e0c59..50fbd770 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -17,6 +17,8 @@ const routeAuthType = (method: string, route: string) => { route.startsWith("/api/departments") || route.startsWith("/api/officer") || route.startsWith("/api/skill") || + route.startsWith("/api/user") || + route.startsWith("/api/golinks") || route.startsWith("/api/course")) && method != "GET") ) { @@ -35,6 +37,12 @@ const routeAuthType = (method: string, route: string) => { return "None"; }; +const accessDenied = (authType: string) => { + return new NextResponse(`Access Denied; need to be ${authType} to access`, { + status: 403, + }); +}; + export const authMiddleware = async (request: NextRequest) => { // console.log("Auth Middleware is running"); const { pathname } = request.nextUrl; @@ -43,12 +51,17 @@ export const authMiddleware = async (request: NextRequest) => { // slice out the `Bearer ...` const authToken = request.headers.get("Authorization")?.slice(7); const authType = routeAuthType(method, pathname); + console.log(authType); if (authType == "None") { // console.log("Letting through with no auth"); return NextResponse.next(); } + if (authToken == null) { + return accessDenied(authType); + } + const perm_fetch = await fetch(process.env.NEXTAUTH_URL + "/api/authLevel", { body: JSON.stringify({ token: authToken }), method: "PUT", @@ -73,7 +86,5 @@ export const authMiddleware = async (request: NextRequest) => { return NextResponse.next(); } // console.log("Access Denied"); - return new NextResponse(`Access Denied; need to be ${authType} to access`, { - status: 403, - }); + return accessDenied(authType); }; From 182c8269110748cda9266bda25dc49353074f252 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 16:54:06 -0500 Subject: [PATCH 06/28] update postman tests to include auth --- ...iteTheSSEquelTests.postman_collection.json | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/postman/WebsiteTheSSEquelTests.postman_collection.json b/postman/WebsiteTheSSEquelTests.postman_collection.json index f43aedcd..97506d42 100644 --- a/postman/WebsiteTheSSEquelTests.postman_collection.json +++ b/postman/WebsiteTheSSEquelTests.postman_collection.json @@ -108,6 +108,55 @@ } } ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer 123", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": 1,\r\n \"name\": \"Johnathaniel\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:3000/api/user/", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "user", + "" + ] + } + }, + "response": [] + }, + { + "name": "/api/user no auth", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response for PUT /api/user without auth\", function () {\r", + " pm.expect(pm.response.code).to.equal(403);\r", + " pm.expect(pm.response.text()).to.equal(\"Access Denied; need to be Officer to access\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], "request": { "method": "PUT", "header": [], @@ -121,14 +170,15 @@ } }, "url": { - "raw": "localhost:3000/api/user", + "raw": "localhost:3000/api/user/", "host": [ "localhost" ], "port": "3000", "path": [ "api", - "user" + "user", + "" ] } }, @@ -246,7 +296,13 @@ ], "request": { "method": "PUT", - "header": [], + "header": [ + { + "key": "Authorization", + "value": "Bearer 123", + "type": "text" + } + ], "body": { "mode": "raw", "raw": "{\r\n \"id\": 1,\r\n \"hourBlockId\": 2\r\n}", @@ -257,14 +313,15 @@ } }, "url": { - "raw": "localhost:3000/api/schedule", + "raw": "localhost:3000/api/schedule/", "host": [ "localhost" ], "port": "3000", "path": [ "api", - "schedule" + "schedule", + "" ] } }, From b8c0181beda5ad5b9b45618756dcc510ac4e27d3 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 16:54:33 -0500 Subject: [PATCH 07/28] update postman tests to run on dev/auth-api --- .github/workflows/postman-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/postman-tests.yml b/.github/workflows/postman-tests.yml index f5d570e9..0abf3399 100644 --- a/.github/workflows/postman-tests.yml +++ b/.github/workflows/postman-tests.yml @@ -2,7 +2,7 @@ name: Postman tests on: push: - branches: [main, dev/postman-tests, dev/api] + branches: [main, dev/postman-tests, dev/api, dev/auth-api] jobs: run-and-test: runs-on: ubuntu-latest From 74f75070d71f07e6bdf62761d7c6d111d8d670b1 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 16:59:41 -0500 Subject: [PATCH 08/28] add dummy auth to postman tests that need it --- ...ebsiteTheSSEquelTests.postman_collection.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/postman/WebsiteTheSSEquelTests.postman_collection.json b/postman/WebsiteTheSSEquelTests.postman_collection.json index 97506d42..836eb1f4 100644 --- a/postman/WebsiteTheSSEquelTests.postman_collection.json +++ b/postman/WebsiteTheSSEquelTests.postman_collection.json @@ -383,7 +383,13 @@ ], "request": { "method": "GET", - "header": [], + "header": [ + { + "key": "Authorization", + "value": "Bearer 123", + "type": "text" + } + ], "url": { "raw": "localhost:3000/api/golinks/officer", "host": [ @@ -418,7 +424,13 @@ ], "request": { "method": "PUT", - "header": [], + "header": [ + { + "key": "Authorization", + "value": "Bearer 123", + "type": "text" + } + ], "body": { "mode": "raw", "raw": "{\r\n \"id\": 1,\r\n \"url\": \"https://duckduckgo.com\",\r\n \"golink\": \"Duck Duck Go\"\r\n}", From df680a3340ec08c8c45a3040a67a47e16c49cf73 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 17:26:58 -0500 Subject: [PATCH 09/28] log server output --- .github/workflows/postman-tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/postman-tests.yml b/.github/workflows/postman-tests.yml index 0abf3399..2ca3e450 100644 --- a/.github/workflows/postman-tests.yml +++ b/.github/workflows/postman-tests.yml @@ -42,7 +42,7 @@ jobs: - name: Start API run: | cd next - npm run dev & + npm run dev > server_log.txt & # Run the tests through postman - name: Run API Tests @@ -54,4 +54,6 @@ jobs: reporters: cli - name: Output summary to console - run: echo ${{ steps.run-newman.outputs.summary }} + run: | + echo ${{ steps.run-newman.outputs.summary }} + cat server_log.txt From d636b19f159e043fc2e9fcdb8238ca248ef358f8 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 17:31:01 -0500 Subject: [PATCH 10/28] postman CI - always show logs --- .github/workflows/postman-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/postman-tests.yml b/.github/workflows/postman-tests.yml index 2ca3e450..1eb81491 100644 --- a/.github/workflows/postman-tests.yml +++ b/.github/workflows/postman-tests.yml @@ -54,6 +54,7 @@ jobs: reporters: cli - name: Output summary to console + if: always() run: | echo ${{ steps.run-newman.outputs.summary }} cat server_log.txt From 3897dde405f093419f61c182e7e0018b566adfb7 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 17:36:31 -0500 Subject: [PATCH 11/28] maybe this will print out the logs --- .github/workflows/postman-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/postman-tests.yml b/.github/workflows/postman-tests.yml index 1eb81491..283b07c0 100644 --- a/.github/workflows/postman-tests.yml +++ b/.github/workflows/postman-tests.yml @@ -42,7 +42,7 @@ jobs: - name: Start API run: | cd next - npm run dev > server_log.txt & + npm run dev --logs-dir=server_logs/ & # Run the tests through postman - name: Run API Tests @@ -57,4 +57,4 @@ jobs: if: always() run: | echo ${{ steps.run-newman.outputs.summary }} - cat server_log.txt + cat server_logs/* From 0ad4245121a10db212a1e1e79f6d2a82090874fa Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 2 Mar 2024 17:46:54 -0500 Subject: [PATCH 12/28] add env vars for postman CI --- .github/workflows/postman-tests.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/postman-tests.yml b/.github/workflows/postman-tests.yml index 283b07c0..2f812c69 100644 --- a/.github/workflows/postman-tests.yml +++ b/.github/workflows/postman-tests.yml @@ -40,9 +40,13 @@ jobs: # run the API in the background. This will host it on localhost:3000 - name: Start API + env: + NEXTAUTH_URL: "http://localhost:3000" + # this doesn't actually have to be a secret on CI + NEXTAUTH_SECRET: "123" run: | cd next - npm run dev --logs-dir=server_logs/ & + npm run dev & # Run the tests through postman - name: Run API Tests @@ -57,4 +61,3 @@ jobs: if: always() run: | echo ${{ steps.run-newman.outputs.summary }} - cat server_logs/* From 72fc6fb4223565e48bf7f99f109475dced509b74 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 3 Mar 2024 14:12:46 -0500 Subject: [PATCH 13/28] get rid of ID in the seed argh --- next/prisma/seed.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/next/prisma/seed.ts b/next/prisma/seed.ts index c4e6cef4..795a8a20 100644 --- a/next/prisma/seed.ts +++ b/next/prisma/seed.ts @@ -437,7 +437,6 @@ async function seedAccount() { where: { id: 1 }, update: {}, create: { - id: 1, userId: 1, type: "google", provider: "google.com", @@ -455,7 +454,6 @@ async function seedAccount() { where: { id: 2 }, update: {}, create: { - id: 2, userId: 2, type: "google", provider: "google.com", @@ -473,7 +471,6 @@ async function seedAccount() { where: { id: 3 }, update: {}, create: { - id: 3, userId: 3, type: "google", provider: "google.com", From 2e19a47f8c19b416d41a60cab4417028a5491a86 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 3 Mar 2024 14:44:10 -0500 Subject: [PATCH 14/28] GET RID OF CREATE ID IN PRISMA SEED --- next/prisma/seed.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/next/prisma/seed.ts b/next/prisma/seed.ts index 795a8a20..951b2b2e 100644 --- a/next/prisma/seed.ts +++ b/next/prisma/seed.ts @@ -100,7 +100,6 @@ async function seedOfficer() { where: { id: 1 }, update: {}, create: { - id: 1, position_id: 1, user_id: 1, is_active: true, @@ -113,7 +112,6 @@ async function seedOfficer() { where: { id: 2 }, update: {}, create: { - id: 2, position_id: 2, user_id: 2, is_active: true, @@ -126,7 +124,6 @@ async function seedOfficer() { where: { id: 3 }, update: {}, create: { - id: 3, position_id: 3, user_id: 1, is_active: false, @@ -142,7 +139,6 @@ async function seedMentor() { where: { id: 1 }, update: {}, create: { - id: 1, user_Id: 1, expirationDate: new Date(), isActive: true, @@ -152,7 +148,6 @@ async function seedMentor() { where: { id: 2 }, update: {}, create: { - id: 2, user_Id: 2, expirationDate: new Date(), isActive: true, @@ -163,7 +158,6 @@ async function seedMentor() { where: { id: 3 }, update: {}, create: { - id: 3, user_Id: 3, expirationDate: new Date(), isActive: true, @@ -236,7 +230,6 @@ async function seedDepartment() { where: { id: 1 }, update: { shortTitle: "CS" }, create: { - id: 1, title: "Computer Science", shortTitle: "CS", }, @@ -245,7 +238,6 @@ async function seedDepartment() { where: { id: 2 }, update: { shortTitle: "SWEN" }, create: { - id: 2, title: "Software Engineering", shortTitle: "SWEN", }, @@ -254,7 +246,6 @@ async function seedDepartment() { where: { id: 3 }, update: { shortTitle: "IGM" }, create: { - id: 3, title: "Interactive Games and Media", shortTitle: "IGM", }, @@ -268,7 +259,6 @@ async function seedCourse() { where: { id: 1 }, update: {}, create: { - id: 1, title: "Software Development I", departmentId: 2, code: 123, @@ -278,7 +268,6 @@ async function seedCourse() { where: { id: 2 }, update: {}, create: { - id: 2, title: "Software Development II", departmentId: 2, code: 124, @@ -288,7 +277,6 @@ async function seedCourse() { where: { id: 3 }, update: {}, create: { - id: 3, title: "CS For AP Students", departmentId: 1, code: 140, @@ -302,7 +290,6 @@ async function seedCourseTaken() { where: { id: 1 }, update: {}, create: { - id: 1, mentorId: 1, courseId: 1, }, @@ -311,7 +298,6 @@ async function seedCourseTaken() { where: { id: 2 }, update: {}, create: { - id: 2, mentorId: 2, courseId: 2, }, @@ -320,7 +306,6 @@ async function seedCourseTaken() { where: { id: 3 }, update: {}, create: { - id: 3, mentorId: 3, courseId: 3, }, @@ -361,7 +346,6 @@ async function seedSchedule() { where: { id: 1 }, update: {}, create: { - id: 1, mentorId: 1, hourBlockId: 1, }, @@ -370,7 +354,6 @@ async function seedSchedule() { where: { id: 2 }, update: {}, create: { - id: 2, mentorId: 2, hourBlockId: 2, }, @@ -379,7 +362,6 @@ async function seedSchedule() { where: { id: 3 }, update: {}, create: { - id: 3, mentorId: 3, hourBlockId: 3, }, @@ -405,7 +387,6 @@ async function seedGoLinks() { where: { id: 2 }, update: {}, create: { - id: 2, golink: "google", url: "google.com", description: "An underground and unknown search engine", @@ -419,7 +400,6 @@ async function seedGoLinks() { where: { id: 3 }, update: {}, create: { - id: 3, golink: "youtube", url: "youtube.com", description: "A small video sharing website", @@ -527,7 +507,6 @@ async function seedVerificationToken() { where: { id: 1 }, update: {}, create: { - id: 1, identifier: "sadsad", token: "123", expires: new Date(), @@ -537,7 +516,6 @@ async function seedVerificationToken() { where: { id: 2 }, update: {}, create: { - id: 2, identifier: "qwewqr", token: "124", expires: new Date(), @@ -547,7 +525,6 @@ async function seedVerificationToken() { where: { id: 3 }, update: {}, create: { - id: 3, identifier: "wsx", token: "125", expires: new Date(), From 4fa18a24bd25091e1a4c9c73f9472c5d1eab151d Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 23 Mar 2024 16:10:35 -0400 Subject: [PATCH 15/28] remove prisma from auth middleware --- next/lib/middlewares/authentication.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index 50fbd770..5e402286 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -1,8 +1,5 @@ -import { PrismaClient } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; -const prisma = new PrismaClient(); - /** * check the URL to see what level of authorization is required * @param route the API route to check From 8278eda69625f822123c49f14718facb78b530d2 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 23 Mar 2024 16:54:42 -0400 Subject: [PATCH 16/28] create cursed functional auth middlewares --- next/lib/middlewares/authentication.ts | 67 ++++++++++++++++++++++++++ next/middleware.ts | 5 +- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index 5e402286..d586c1fa 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -1,5 +1,56 @@ import { NextRequest, NextResponse } from "next/server"; +/** + * A function to verify if a request should be let through. This function should handle the required authLevel + * API calls and returns a boolean representing whether or not the request should be allowed through + */ +type AuthVerifier = (method: string, request: NextRequest) => Promise; + +/** + * Creates an AuthVerifier that checks a property of the user's permissions. Handles the API call + * and bearer token automatically + * @param verifier function that takes the HTTP method used and the user's permissions and returns + * a boolean representing whether or not the request should be allowed through + * @returns an AuthVerifier that checks the relevant permissions for the user + */ +const authVerifierFactory = ( + verifier: (method: string, permissions: any) => boolean +): AuthVerifier => { + return async (method: string, request: NextRequest) => { + // slice out the `Bearer ...` + const authToken = request.headers.get("Authorization")?.slice(7); + // fetch permissions from the API + const perm_fetch = await fetch( + process.env.NEXTAUTH_URL + "/api/authLevel", + { + body: JSON.stringify({ token: authToken }), + method: "PUT", + } + ); + // console.log(perm_fetch); + const permissions = await perm_fetch.json(); + return verifier(method, permissions); + }; +}; + +/** + * Auth verifier that makes sure the user is an officer + */ +const officerVerifier = authVerifierFactory( + (_, permissions) => permissions.isOfficer +); + +/** + * Map from API route name to authorization verifier. The verifier should be run against any request that + * goes through that route. + * Keys are the second element in the path segment; for example, the path "/api/golinks/officer" would + * correspond to the key "golinks" + */ +const ROUTES: { [key: string]: AuthVerifier } = { + hourBlocks: officerVerifier, + user: officerVerifier, +}; + /** * check the URL to see what level of authorization is required * @param route the API route to check @@ -40,6 +91,22 @@ const accessDenied = (authType: string) => { }); }; +export const experimentalAuthMiddleware = async (request: NextRequest) => { + const { pathname } = request.nextUrl; + const [_, apiSegment, pathSegment] = pathname.split("/"); + if (apiSegment != "api") { + return NextResponse.next(); + } + const routeAuth = ROUTES[pathSegment]; + if (routeAuth == null) { + return NextResponse.next(); + } + if (await routeAuth(request.method, request)) { + return NextResponse.next(); + } + return new NextResponse("Access Denied :(", { status: 403 }); +}; + export const authMiddleware = async (request: NextRequest) => { // console.log("Auth Middleware is running"); const { pathname } = request.nextUrl; diff --git a/next/middleware.ts b/next/middleware.ts index 561a8d1d..d93868fe 100644 --- a/next/middleware.ts +++ b/next/middleware.ts @@ -1,11 +1,12 @@ import { NextRequest, NextResponse } from "next/server"; import { golinksMiddleware } from "./lib/middlewares/golinks"; -import { authMiddleware } from "./lib/middlewares/authentication"; +import { authMiddleware, experimentalAuthMiddleware } from "./lib/middlewares/authentication"; export async function middleware(request: NextRequest) { console.log("Middleware is running on", request.url); // Run the authentication middleware logic - let response = await authMiddleware(request); + // let response = await authMiddleware(request); + let response = await experimentalAuthMiddleware(request); // If the response is not NextResponse.next(), terminate the middleware chain // and return the response. This would occur if the middleware redirects or rewrites. if (response.headers.get("x-middleware-next") != "1") { From afd94c84aaeca44f2ddeaa743fe5f8ed47a1e754 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 24 Mar 2024 13:49:28 -0400 Subject: [PATCH 17/28] trim parameters and expand return type for AuthVerifier --- next/lib/middlewares/authentication.ts | 30 +++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index d586c1fa..fe5a2ead 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -4,7 +4,9 @@ import { NextRequest, NextResponse } from "next/server"; * A function to verify if a request should be let through. This function should handle the required authLevel * API calls and returns a boolean representing whether or not the request should be allowed through */ -type AuthVerifier = (method: string, request: NextRequest) => Promise; +type AuthVerifier = ( + request: NextRequest +) => Promise<{ isAllowed: boolean; authType: string }>; /** * Creates an AuthVerifier that checks a property of the user's permissions. Handles the API call @@ -14,31 +16,32 @@ type AuthVerifier = (method: string, request: NextRequest) => Promise; * @returns an AuthVerifier that checks the relevant permissions for the user */ const authVerifierFactory = ( - verifier: (method: string, permissions: any) => boolean + verifier: ( + method: string, + permissions: any + ) => { isAllowed: boolean; authType: string } ): AuthVerifier => { - return async (method: string, request: NextRequest) => { + return async (request: NextRequest) => { // slice out the `Bearer ...` const authToken = request.headers.get("Authorization")?.slice(7); // fetch permissions from the API - const perm_fetch = await fetch( + const permissions = await fetch( process.env.NEXTAUTH_URL + "/api/authLevel", { body: JSON.stringify({ token: authToken }), method: "PUT", } - ); - // console.log(perm_fetch); - const permissions = await perm_fetch.json(); - return verifier(method, permissions); + ).then(async (res) => await res.json()); + return verifier(request.method, permissions); }; }; /** * Auth verifier that makes sure the user is an officer */ -const officerVerifier = authVerifierFactory( - (_, permissions) => permissions.isOfficer -); +const officerVerifier = authVerifierFactory((_, permissions) => { + return { isAllowed: permissions.isOfficer, authType: "Officer" }; +}); /** * Map from API route name to authorization verifier. The verifier should be run against any request that @@ -101,10 +104,11 @@ export const experimentalAuthMiddleware = async (request: NextRequest) => { if (routeAuth == null) { return NextResponse.next(); } - if (await routeAuth(request.method, request)) { + const { isAllowed, authType } = await routeAuth(request); + if (isAllowed) { return NextResponse.next(); } - return new NextResponse("Access Denied :(", { status: 403 }); + return accessDenied(authType); }; export const authMiddleware = async (request: NextRequest) => { From b6d93456863ef6838d98de6ffd41eff713392499 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 24 Mar 2024 14:02:23 -0400 Subject: [PATCH 18/28] add more auth routes --- next/lib/middlewares/authentication.ts | 70 +++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index fe5a2ead..0e862ad0 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -16,10 +16,7 @@ type AuthVerifier = ( * @returns an AuthVerifier that checks the relevant permissions for the user */ const authVerifierFactory = ( - verifier: ( - method: string, - permissions: any - ) => { isAllowed: boolean; authType: string } + verifier: (permissions: any) => { isAllowed: boolean; authType: string } ): AuthVerifier => { return async (request: NextRequest) => { // slice out the `Bearer ...` @@ -32,17 +29,66 @@ const authVerifierFactory = ( method: "PUT", } ).then(async (res) => await res.json()); - return verifier(request.method, permissions); + return verifier(permissions); + }; +}; + +/** + * Creates an AuthVerifier that checks a property of the user's permissions. Handles the API call + * and bearer token automatically + * @param verifier function that takes the HTTP method used and the user's permissions and returns + * a boolean representing whether or not the request should be allowed through + * @returns an AuthVerifier that checks the relevant permissions for the user + */ +const nonGetAuthVerifierFactory = ( + verifier: (permissions: any) => { isAllowed: boolean; authType: string } +): AuthVerifier => { + return async (request: NextRequest) => { + // if it's a GET, just allow it + if (request.method === "GET") { + return { isAllowed: true, authType: "None" }; + } + // slice out the `Bearer ...` + const authToken = request.headers.get("Authorization")?.slice(7); + // fetch permissions from the API + const permissions = await fetch( + process.env.NEXTAUTH_URL + "/api/authLevel", + { + body: JSON.stringify({ token: authToken }), + method: "PUT", + } + ).then(async (res) => await res.json()); + return verifier(permissions); }; }; /** * Auth verifier that makes sure the user is an officer */ -const officerVerifier = authVerifierFactory((_, permissions) => { +const officerVerifier = authVerifierFactory((permissions) => { return { isAllowed: permissions.isOfficer, authType: "Officer" }; }); +/** + * Auth verifier that makes sure the user is an officer unless using a GET method + */ +const nonGetOfficerVerifier = nonGetAuthVerifierFactory((permissions) => { + return { + isAllowed: permissions.isOfficer, + authType: "Officer", + }; +}); + +/** + * Auth verifier that makes sure the user is an officer unless using a GET method + */ +const nonGetMentorVerifier = nonGetAuthVerifierFactory((permissions) => { + return { + isAllowed: permissions.isMentor, + authType: "Mentor", + }; +}); + /** * Map from API route name to authorization verifier. The verifier should be run against any request that * goes through that route. @@ -50,8 +96,16 @@ const officerVerifier = authVerifierFactory((_, permissions) => { * correspond to the key "golinks" */ const ROUTES: { [key: string]: AuthVerifier } = { - hourBlocks: officerVerifier, - user: officerVerifier, + hourBlocks: nonGetOfficerVerifier, + departments: nonGetOfficerVerifier, + officer: nonGetOfficerVerifier, + skill: nonGetOfficerVerifier, + user: nonGetOfficerVerifier, + golinks: nonGetOfficerVerifier, + course: nonGetOfficerVerifier, + schedule: nonGetMentorVerifier, + mentor: nonGetMentorVerifier, + courseTaken: nonGetMentorVerifier, }; /** From e7803a28445c26d7787d8247afe676c727445184 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 24 Mar 2024 14:12:55 -0400 Subject: [PATCH 19/28] add golinks route verifier --- next/lib/middlewares/authentication.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index 0e862ad0..051c70d1 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -89,6 +89,18 @@ const nonGetMentorVerifier = nonGetAuthVerifierFactory((permissions) => { }; }); +const goLinkVerifier = async (request: NextRequest) => { + // if it's a GET to a public route, just allow it + if ( + request.method === "GET" && + !request.nextUrl.toString().startsWith("/api/golinks/officer") + ) { + return { isAllowed: true, authType: "None" }; + } + // otherwise, run the officer verifier + return officerVerifier(request); +}; + /** * Map from API route name to authorization verifier. The verifier should be run against any request that * goes through that route. @@ -101,7 +113,7 @@ const ROUTES: { [key: string]: AuthVerifier } = { officer: nonGetOfficerVerifier, skill: nonGetOfficerVerifier, user: nonGetOfficerVerifier, - golinks: nonGetOfficerVerifier, + golinks: goLinkVerifier, course: nonGetOfficerVerifier, schedule: nonGetMentorVerifier, mentor: nonGetMentorVerifier, From 6d8afa95ae8b7ec29b7e84631c539ea4e449328f Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 24 Mar 2024 14:24:18 -0400 Subject: [PATCH 20/28] better docs for new auth stuff --- next/lib/middlewares/authentication.ts | 59 ++++++++------------------ 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index 051c70d1..13732a75 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -11,8 +11,8 @@ type AuthVerifier = ( /** * Creates an AuthVerifier that checks a property of the user's permissions. Handles the API call * and bearer token automatically - * @param verifier function that takes the HTTP method used and the user's permissions and returns - * a boolean representing whether or not the request should be allowed through + * @param verifier function that takes the user's permissions and returns + * a boolean representing whether or not the request should be allowed through and the number * @returns an AuthVerifier that checks the relevant permissions for the user */ const authVerifierFactory = ( @@ -36,29 +36,17 @@ const authVerifierFactory = ( /** * Creates an AuthVerifier that checks a property of the user's permissions. Handles the API call * and bearer token automatically - * @param verifier function that takes the HTTP method used and the user's permissions and returns + * @param innerVerifier function that takes the HTTP request used and returns * a boolean representing whether or not the request should be allowed through * @returns an AuthVerifier that checks the relevant permissions for the user */ -const nonGetAuthVerifierFactory = ( - verifier: (permissions: any) => { isAllowed: boolean; authType: string } -): AuthVerifier => { +const nonGetVerifier = (innerVerifier: AuthVerifier): AuthVerifier => { return async (request: NextRequest) => { // if it's a GET, just allow it if (request.method === "GET") { return { isAllowed: true, authType: "None" }; } - // slice out the `Bearer ...` - const authToken = request.headers.get("Authorization")?.slice(7); - // fetch permissions from the API - const permissions = await fetch( - process.env.NEXTAUTH_URL + "/api/authLevel", - { - body: JSON.stringify({ token: authToken }), - method: "PUT", - } - ).then(async (res) => await res.json()); - return verifier(permissions); + return innerVerifier(request); }; }; @@ -70,23 +58,10 @@ const officerVerifier = authVerifierFactory((permissions) => { }); /** - * Auth verifier that makes sure the user is an officer unless using a GET method - */ -const nonGetOfficerVerifier = nonGetAuthVerifierFactory((permissions) => { - return { - isAllowed: permissions.isOfficer, - authType: "Officer", - }; -}); - -/** - * Auth verifier that makes sure the user is an officer unless using a GET method + * Auth verifier that makes sure the user is a mentor */ -const nonGetMentorVerifier = nonGetAuthVerifierFactory((permissions) => { - return { - isAllowed: permissions.isMentor, - authType: "Mentor", - }; +const mentorVerifier = authVerifierFactory((permissions) => { + return { isAllowed: permissions.isMentor, authType: "Mentor" }; }); const goLinkVerifier = async (request: NextRequest) => { @@ -108,16 +83,16 @@ const goLinkVerifier = async (request: NextRequest) => { * correspond to the key "golinks" */ const ROUTES: { [key: string]: AuthVerifier } = { - hourBlocks: nonGetOfficerVerifier, - departments: nonGetOfficerVerifier, - officer: nonGetOfficerVerifier, - skill: nonGetOfficerVerifier, - user: nonGetOfficerVerifier, + hourBlocks: nonGetVerifier(officerVerifier), + departments: nonGetVerifier(officerVerifier), + officer: nonGetVerifier(officerVerifier), + skill: nonGetVerifier(officerVerifier), + user: nonGetVerifier(officerVerifier), golinks: goLinkVerifier, - course: nonGetOfficerVerifier, - schedule: nonGetMentorVerifier, - mentor: nonGetMentorVerifier, - courseTaken: nonGetMentorVerifier, + course: nonGetVerifier(officerVerifier), + schedule: nonGetVerifier(mentorVerifier), + mentor: nonGetVerifier(mentorVerifier), + courseTaken: nonGetVerifier(mentorVerifier), }; /** From 80bb7890c57126df8780ff29a737b3549e42c50a Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 30 Mar 2024 16:19:01 -0400 Subject: [PATCH 21/28] DRY auth verifiers --- next/lib/middlewares/authentication.ts | 47 ++++++++++++++++---------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/next/lib/middlewares/authentication.ts b/next/lib/middlewares/authentication.ts index 13732a75..efc86716 100644 --- a/next/lib/middlewares/authentication.ts +++ b/next/lib/middlewares/authentication.ts @@ -4,9 +4,9 @@ import { NextRequest, NextResponse } from "next/server"; * A function to verify if a request should be let through. This function should handle the required authLevel * API calls and returns a boolean representing whether or not the request should be allowed through */ -type AuthVerifier = ( - request: NextRequest -) => Promise<{ isAllowed: boolean; authType: string }>; +type AuthVerifier = (request: NextRequest) => Promise; + +type AuthOutput = { isAllowed: boolean; authType: string }; /** * Creates an AuthVerifier that checks a property of the user's permissions. Handles the API call @@ -16,7 +16,7 @@ type AuthVerifier = ( * @returns an AuthVerifier that checks the relevant permissions for the user */ const authVerifierFactory = ( - verifier: (permissions: any) => { isAllowed: boolean; authType: string } + verifier: (permissions: any) => AuthOutput ): AuthVerifier => { return async (request: NextRequest) => { // slice out the `Bearer ...` @@ -34,11 +34,7 @@ const authVerifierFactory = ( }; /** - * Creates an AuthVerifier that checks a property of the user's permissions. Handles the API call - * and bearer token automatically - * @param innerVerifier function that takes the HTTP request used and returns - * a boolean representing whether or not the request should be allowed through - * @returns an AuthVerifier that checks the relevant permissions for the user + * A wrapper around another verifier that allows GET requests without verification */ const nonGetVerifier = (innerVerifier: AuthVerifier): AuthVerifier => { return async (request: NextRequest) => { @@ -64,6 +60,9 @@ const mentorVerifier = authVerifierFactory((permissions) => { return { isAllowed: permissions.isMentor, authType: "Mentor" }; }); +/** + * Auth verifier specifically for the golinks route + */ const goLinkVerifier = async (request: NextRequest) => { // if it's a GET to a public route, just allow it if ( @@ -76,6 +75,16 @@ const goLinkVerifier = async (request: NextRequest) => { return officerVerifier(request); }; +/** + * An auth verifier that allows GET requests but makes sure all other requests are made by officers + */ +const nonGetOfficerVerifier = nonGetVerifier(officerVerifier); + +/** + * An auth verifier that allows GET requests but makes sure all other requests are made by mentors + */ +const nonGetMentorVerifier = nonGetVerifier(mentorVerifier); + /** * Map from API route name to authorization verifier. The verifier should be run against any request that * goes through that route. @@ -83,16 +92,18 @@ const goLinkVerifier = async (request: NextRequest) => { * correspond to the key "golinks" */ const ROUTES: { [key: string]: AuthVerifier } = { - hourBlocks: nonGetVerifier(officerVerifier), - departments: nonGetVerifier(officerVerifier), - officer: nonGetVerifier(officerVerifier), - skill: nonGetVerifier(officerVerifier), - user: nonGetVerifier(officerVerifier), + course: nonGetOfficerVerifier, + courseTaken: nonGetMentorVerifier, + departments: nonGetOfficerVerifier, golinks: goLinkVerifier, - course: nonGetVerifier(officerVerifier), - schedule: nonGetVerifier(mentorVerifier), - mentor: nonGetVerifier(mentorVerifier), - courseTaken: nonGetVerifier(mentorVerifier), + hourBlocks: nonGetOfficerVerifier, + mentor: nonGetOfficerVerifier, + mentorSkill: nonGetMentorVerifier, + officer: nonGetOfficerVerifier, + quotes: nonGetOfficerVerifier, + schedule: nonGetMentorVerifier, + skill: nonGetOfficerVerifier, + user: nonGetOfficerVerifier, }; /** From bd78153907a27b00364e424c4718f08768cb5c9e Mon Sep 17 00:00:00 2001 From: Christian Date: Sun, 31 Mar 2024 14:49:02 -0400 Subject: [PATCH 22/28] Edit and delete possibly working??? --- next/app/go/GoLink.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/next/app/go/GoLink.tsx b/next/app/go/GoLink.tsx index 96c7b1e9..3b0c5e3f 100644 --- a/next/app/go/GoLink.tsx +++ b/next/app/go/GoLink.tsx @@ -3,7 +3,7 @@ import { GoLinkStar } from "@/components/common/Icons"; import { GoLinkEdit } from "@/components/common/Icons"; import { GoLinkDelete } from "@/components/common/Icons"; import { useSession } from "next-auth/react"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; export interface GoLinkProps { id: number; @@ -222,7 +222,17 @@ const GoLink: React.FC = ({ id, goUrl, url, description, pinned, fe const EditAndDelete: React.FC = ({ id, goUrl, url, description, pinned } ) => { const { data: session } = useSession(); - if(session) { + const [isOfficer, setIsOfficer] = useState(false); + + useEffect(() => { + useCallback(async() => { + const response = await fetch("http://localhost:3000/api/authLevel", {body: JSON.stringify({email: session?.user?.email}), method: "PUT"}); + const data = await response.json(); + setIsOfficer(data.isOfficer); + }, []) + }); + + if(session && isOfficer) { return (
From 407dd5b486fc8628040a7bc0097a82db4f02e340 Mon Sep 17 00:00:00 2001 From: Christian Date: Sun, 31 Mar 2024 15:15:37 -0400 Subject: [PATCH 23/28] GoLink Auth :) --- next/app/api/auth/[...nextauth]/route.ts | 19 +++++++++++++++---- next/app/go/GoLink.tsx | 14 ++++++-------- next/app/go/MakeNewGoLink.tsx | 21 +++++++++++++++------ 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/next/app/api/auth/[...nextauth]/route.ts b/next/app/api/auth/[...nextauth]/route.ts index 1beac28a..d5d298be 100644 --- a/next/app/api/auth/[...nextauth]/route.ts +++ b/next/app/api/auth/[...nextauth]/route.ts @@ -19,10 +19,21 @@ export const authOptions: AuthOptions = { }) ], callbacks: { - session: async ({ session, user }) => { - // fetch user roles from database - // session.roles = ... - return Promise.resolve(session) + jwt: async({ token, user } : any) => { + // the user object is what returned from the Credentials login, it has `accessToken` from the server `/login` endpoint + // assign the accessToken to the `token` object, so it will be available on the `session` callback + if (user) { + token.accessToken = user.accessToken + } + return token + }, + session: async({ session, token } : any) => { + // the token object is what returned from the `jwt` callback, it has the `accessToken` that we assigned before + // Assign the accessToken to the `session` object, so it will be available on our app through `useSession` hooks + if (token) { + session.accessToken = token.accessToken + } + return session } } }; diff --git a/next/app/go/GoLink.tsx b/next/app/go/GoLink.tsx index 3b0c5e3f..90ecd7e2 100644 --- a/next/app/go/GoLink.tsx +++ b/next/app/go/GoLink.tsx @@ -224,15 +224,13 @@ const EditAndDelete: React.FC = ({ id, goUrl, url, description, pin const { data: session } = useSession(); const [isOfficer, setIsOfficer] = useState(false); - useEffect(() => { - useCallback(async() => { - const response = await fetch("http://localhost:3000/api/authLevel", {body: JSON.stringify({email: session?.user?.email}), method: "PUT"}); - const data = await response.json(); - setIsOfficer(data.isOfficer); - }, []) - }); + useCallback(async() => { + const response = await fetch("http://localhost:3000/api/authLevel", {body: JSON.stringify({email: session?.user?.email}), method: "PUT"}); + const data = await response.json(); + setIsOfficer(data.isOfficer); + }, []) - if(session && isOfficer) { + if(isOfficer) { return (
diff --git a/next/app/go/MakeNewGoLink.tsx b/next/app/go/MakeNewGoLink.tsx index 36919df1..6ae32370 100644 --- a/next/app/go/MakeNewGoLink.tsx +++ b/next/app/go/MakeNewGoLink.tsx @@ -1,9 +1,9 @@ import { useSession } from "next-auth/react"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { CreateGoLinkProps } from "./page"; export const GoLinkButton: React.FC = ({fetchData}) => { - const { data: session } = useSession() + const { data: session } : any= useSession() const [title, setTitle] = useState(""); const [url, setUrl] = useState(""); const [description, setDescription] = useState(""); @@ -29,6 +29,7 @@ export const GoLinkButton: React.FC = ({fetchData}) => { method: 'POST', headers: { 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + session?.accessToken }, // In body, make sure parameter names MATCH the ones in api/golinks/route.ts for the POST request // Left is backend parameter names, right is our front end names @@ -52,15 +53,23 @@ export const GoLinkButton: React.FC = ({fetchData}) => { } catch (error) {} }; - if(session){ + const [isOfficer, setIsOfficer] = useState(false); + useCallback(async() => { + const response = await fetch("http://localhost:3000/api/authLevel", {body: JSON.stringify({email: session?.user?.email}), method: "PUT"}); + const data = await response.json(); + setIsOfficer(data.isOfficer); + }, []) + + + if(isOfficer){ return ( <>