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

API Auth #117

Merged
merged 30 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6213c3f
add authLevel route
PokeJofeJr4th Feb 18, 2024
3bb6756
initial auth middleware
PokeJofeJr4th Feb 18, 2024
bf6a18e
Add authentication middleware proper, fix handling of multiple middle…
PokeJofeJr4th Feb 25, 2024
1cc502e
better db statement
PokeJofeJr4th Mar 2, 2024
0078be1
remove vulnerability where a user without an auth token would have of…
PokeJofeJr4th Mar 2, 2024
182c826
update postman tests to include auth
PokeJofeJr4th Mar 2, 2024
b8c0181
update postman tests to run on dev/auth-api
PokeJofeJr4th Mar 2, 2024
74f7507
add dummy auth to postman tests that need it
PokeJofeJr4th Mar 2, 2024
df680a3
log server output
PokeJofeJr4th Mar 2, 2024
d636b19
postman CI - always show logs
PokeJofeJr4th Mar 2, 2024
3897dde
maybe this will print out the logs
PokeJofeJr4th Mar 2, 2024
0ad4245
add env vars for postman CI
PokeJofeJr4th Mar 2, 2024
72fc6fb
get rid of ID in the seed argh
PokeJofeJr4th Mar 3, 2024
2e19a47
GET RID OF CREATE ID IN PRISMA SEED
PokeJofeJr4th Mar 3, 2024
4fa18a2
remove prisma from auth middleware
PokeJofeJr4th Mar 23, 2024
8278eda
create cursed functional auth middlewares
PokeJofeJr4th Mar 23, 2024
afd94c8
trim parameters and expand return type for AuthVerifier
PokeJofeJr4th Mar 24, 2024
b6d9345
add more auth routes
PokeJofeJr4th Mar 24, 2024
e7803a2
add golinks route verifier
PokeJofeJr4th Mar 24, 2024
6d8afa9
better docs for new auth stuff
PokeJofeJr4th Mar 24, 2024
80bb789
DRY auth verifiers
PokeJofeJr4th Mar 30, 2024
bd78153
Edit and delete possibly working???
chris-morgado Mar 31, 2024
e25d074
Merge branch 'dev/golink-auth' into dev/auth-api
chris-morgado Mar 31, 2024
407dd5b
GoLink Auth :)
chris-morgado Mar 31, 2024
cd4926d
correct useCallback usage, update authLevel API to use cookies and GE…
PokeJofeJr4th Apr 6, 2024
f3d256e
ITS ALIVE
PokeJofeJr4th Apr 6, 2024
63cd78f
remove next auth callbacks, delete old auth middleware
PokeJofeJr4th Apr 7, 2024
69eb398
update tests to use cookie
PokeJofeJr4th Apr 7, 2024
d5778a8
probably make tests work
PokeJofeJr4th Apr 7, 2024
4d9bf11
Merge branch 'main' into dev/auth-api
PokeJofeJr4th Apr 14, 2024
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
10 changes: 8 additions & 2 deletions .github/workflows/postman-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,6 +40,10 @@ 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 &
Expand All @@ -54,4 +58,6 @@ jobs:
reporters: cli

- name: Output summary to console
run: echo ${{ steps.run-newman.outputs.summary }}
if: always()
run: |
echo ${{ steps.run-newman.outputs.summary }}
35 changes: 14 additions & 21 deletions next/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,23 @@ import { PrismaClient } from "@prisma/client";
import NextAuth, { AuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";

const prisma = new PrismaClient()
const prisma = new PrismaClient();

export const authOptions: AuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
authorization: {
params: {
hd: "g.rit.edu" // restrict logins to rit.edu accounts
}
}
})
],
callbacks: {
session: async ({ session, user }) => {
// fetch user roles from database
// session.roles = ...
return Promise.resolve(session)
}
}
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
authorization: {
params: {
hd: "g.rit.edu", // restrict logins to rit.edu accounts
},
},
}),
],
};

export const handler = NextAuth(authOptions)
export const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
149 changes: 149 additions & 0 deletions next/app/api/authLevel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { PrismaClient } from "@prisma/client";
import { NextRequest, NextResponse } from "next/server";

const prisma = new PrismaClient();

/**
* 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.
*
* The user's 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 \{token: string}
* @returns \{isUser: boolean, isMember: boolean, isMentor: boolean, isOfficer: boolean} the auth level
*/
export async function PUT(request: Request) {
let body;
try {
body = await request.json();
} catch {
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 (body.token == null) {
return Response.json(authLevel);
}

const user = await prisma.user.findFirst({
where: {
session: {
some: {
sessionToken: body.token,
},
},
},
select: {
// select a minimal amount of data for active mentors
mentor: {
where: { isActive: true },
select: { id: true },
},
// select a minimal amount of data for active officers
officers: {
where: { is_active: true },
select: { id: true },
},
isMember: true,
},
});
if (user != null) {
// deconstruct the user object
const { mentor, officers, isMember } = user;
if (officers.length > 0) {
authLevel.isOfficer = true;
}
if (mentor.length > 0) {
authLevel.isMentor = true;
}
authLevel.isMember = isMember;
authLevel.isUser = true;
}
return Response.json(authLevel);
}

/**
* HTTP GET request to /api/authLevel/
* This route should be used to figure out what level of authorization a given user has.
*
* 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
*/
export async function GET(request: NextRequest) {
const authToken = request.cookies.get("next-auth.session-token")?.value;

const authLevel = {
isUser: false,
isMember: false,
isMentor: false,
isOfficer: false,
};

if (authToken == null) {
return Response.json(authLevel);
}

const user = await prisma.user.findFirst({
where: {
session: {
some: {
sessionToken: authToken,
},
},
},
select: {
// select a minimal amount of data for active mentors
mentor: {
where: { isActive: true },
select: { id: true },
},
// select a minimal amount of data for active officers
officers: {
where: { is_active: true },
select: { id: true },
},
isMember: true,
},
});

if (user != null) {
// deconstruct the user object
const { mentor, officers, isMember } = user;
if (officers.length > 0) {
authLevel.isOfficer = true;
}
if (mentor.length > 0) {
authLevel.isMentor = true;
}
authLevel.isMember = isMember;
authLevel.isUser = true;
}
return Response.json(authLevel);
}
13 changes: 11 additions & 2 deletions next/app/go/GoLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { GoLinkIcon } from "@/components/common/Icons";
import { GoLinkStar } from "@/components/common/Icons";
import { GoLinkEdit } from "@/components/common/Icons";
import { GoLinkDelete } from "@/components/common/Icons";
import { useEffectAsync } from "@/lib/utils";
import { useSession } from "next-auth/react";
import { useState } from "react";
import { useCallback, useEffect, useState } from "react";

export interface GoLinkProps {
id: number;
Expand Down Expand Up @@ -222,7 +223,15 @@ const GoLink: React.FC<GoLinkProps> = ({ id, goUrl, url, description, pinned, fe

const EditAndDelete: React.FC<GoLinkProps> = ({ id, goUrl, url, description, pinned } ) => {
const { data: session } = useSession();
if(session) {
const [isOfficer, setIsOfficer] = useState(false);

useEffectAsync(async() => {
const response = await fetch("http://localhost:3000/api/authLevel");
const data = await response.json();
setIsOfficer(data.isOfficer);
}, []);

if(isOfficer) {
return (
<form>
<div className="flex flex-row">
Expand Down
23 changes: 17 additions & 6 deletions next/app/go/MakeNewGoLink.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useSession } from "next-auth/react";
import { useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { CreateGoLinkProps } from "./page";
import { useEffectAsync } from "@/lib/utils";

export const GoLinkButton: React.FC<CreateGoLinkProps> = ({fetchData}) => {
const { data: session } = useSession()
const { data: session } : any= useSession()
const [title, setTitle] = useState("");
const [url, setUrl] = useState("");
const [description, setDescription] = useState("");
Expand All @@ -29,6 +30,7 @@ export const GoLinkButton: React.FC<CreateGoLinkProps> = ({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
Expand All @@ -52,15 +54,24 @@ export const GoLinkButton: React.FC<CreateGoLinkProps> = ({fetchData}) => {
} catch (error) {}
};

if(session){
const [isOfficer, setIsOfficer] = useState(false);
useEffectAsync(async() => {
const response = await fetch("http://localhost:3000/api/authLevel");
const data = await response.json();
console.log(data);
setIsOfficer(data.isOfficer);
}, []);


if(isOfficer){
return (
<>
<button
onClick={(func) =>{
func.preventDefault();
if(document) {
(document.getElementById('create-golink') as HTMLFormElement).showModal();
}
if(document) {
(document.getElementById('create-golink') as HTMLFormElement).showModal();
}
}
}
className="
Expand Down
Loading
Loading