Skip to content
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
4 changes: 3 additions & 1 deletion .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ GOOGLE_CLIENT_ID=1065709499536-f5g11l8g2v32eu02eh0q6t723uj4pcvv.apps.googleuserc
GOOGLE_CLIENT_SECRET=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
HOST_NAME=localhost
INTERNAL_AUTH=ArandomVeryLongSecret
NOTE=This_file_must_never_contain_production_secrets
NOTE=This_file_must_never_contain_production_secrets
ADMIN_API_KEY=supersecretadminkey
NEXTAUTH_SECRET=anotherlongsecretfornextauth
97 changes: 93 additions & 4 deletions src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import NextAuth from "next-auth";
//import Auth0Provider from "next-auth/providers/auth0";
import GoogleProvider from "next-auth/providers/google";
import type { Session } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { timingSafeEqual } from "crypto";


// Ensures constant-time comparison for strings
const safeCompare = (a, b) => {
if (a.length !== b.length) return false;
return timingSafeEqual(new Uint8Array(Buffer.from(a, "utf8")), new Uint8Array(Buffer.from(b, "utf8")));
};



import {
checkIfUserExists,
generateUser,
getUserByEmail,
} from "../../../lib/backend/database-utils";
import { CustomSession } from "../../../lib/common/types";
//import { canWriteReviews } from "../../../lib/backend/utils";

async function signIn({ profile, user, account }) {
console.log("Sign-in attempt:", { profile, user, account });

if (account.provider === "credentials" && user.isAdminOverrideSession) {
console.log("Admin override sign-in successful for user:", user.email);
return true; // Allow sign-in for admin override sessions without further checks
}


//validate the email is an email
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
Expand Down Expand Up @@ -45,7 +60,17 @@ async function signIn({ profile, user, account }) {
return true;
}

async function session({ session, user }) {
async function session({ session, user, token }): Promise<CustomSession> {
if (token.isAdminOverrideSession) {
console.log("Admin override session detected for user:", session.user.email);
session.user.id = token?.user?.id;
session.user.role = token?.user?.role;
session.user.authorized = token?.user?.authorized;
session.user.admin = token?.user?.admin;
session.user.banned = token?.user?.banned;
return session as CustomSession;
}

const u = await getUserByEmail(session.user.email);

session.user.id = u?.userID;
Expand All @@ -70,12 +95,76 @@ export default NextAuth({
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
checks: "state",
}),
// Admin credentials provider for service accounts
CredentialsProvider({
name: "Admin Credentials",
credentials: {
email: { label: "Assumed Email", type: "text" },
apiKey: { label: "API Key", type: "password" },
role: { label: "Role", type: "text" },
},
async authorize(credentials, req) {
console.log("Attempting admin sign in with credentials:", JSON.stringify(credentials));
const { email, apiKey, role } = credentials;

if (!email || !apiKey || !role) {
throw new Error("Email, API key, and role are required");
}

if (!["student", "faculty"].includes(role)) {
throw new Error("Invalid role");
}

// Compare API key with secret
if (safeCompare(apiKey, process.env.ADMIN_API_KEY)) {

// Email lookup for id matching, otherwise return a default admin user with id 0 (not ideal, but allows for flexibility in admin accounts without database entries)
const existingUser = await getUserByEmail(email);
if (existingUser) {
return {
id: existingUser.userID,
email,
role: existingUser.userType,
authorized: existingUser.canReadReviews as boolean,
admin: existingUser.admin,
banned: existingUser.banned,
isAdminOverrideSession: true, // Custom flag to identify admin sessions
};
}

// Return a user object with admin role
return {
id: '00000000-0000-0000-0000-000000000000',
email,
role: "admin",
authorized: true,
admin: true,
banned: false,
isAdminOverrideSession: true, // Custom flag to identify admin sessions
};
}

throw new Error("Invalid API key");

}
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
signIn: signIn,
session: session,
redirect: redirect,
jwt: async ({ token, user, account }) => {
// If this is an admin override session, add the flag to the token
if ((user as any)?.isAdminOverrideSession) {
token.isAdminOverrideSession = true;
}
if (user) {
token.user = user;
}

return token;
}
},
pages: {
newUser: "/auth/signup",
Expand Down
55 changes: 48 additions & 7 deletions src/pages/auth/signin.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,64 @@
import { getProviders, signIn, useSession } from "next-auth/react";
import Router from "next/router";


export default function SignIn({ providers }) {
const url = typeof window !== "undefined" ? window.location.href : "";
const adminLogin = new URLSearchParams(url.split("?")[1]).get("adminLogin") === "true";

const { data: session, status } = useSession()
if (status === "authenticated") {
Router.push("/");
}



return (
<>
{Object.values(providers).map((provider) => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
))}
{Object.values(providers).map((provider) => {
if (!adminLogin && provider.id === "credentials") {
return null; // Don't show credentials provider on regular login page
}

if (adminLogin && provider.id === "credentials") {
// Render login form
return (
<div key={provider.name}>
<h2>Admin Login</h2>
<form onSubmit={(e) => {
e.preventDefault();
const email = e.target.email.value;
const apiKey = e.target.apiKey.value;
const role = e.target.role.value;
signIn(provider.id, { email, apiKey, role });
}
}>
<div>
<label htmlFor="email">Email:</label>
<input type="text" id="email" name="email" required />
</div>
<div>
<label htmlFor="apiKey">API Key:</label>
<input type="password" id="apiKey" name="apiKey" required />
</div>
<div>
<label htmlFor="role">Role:</label>
<input type="text" id="role" name="role" required />
</div>
<button type="submit">Sign in with {provider.name}</button>
</form>
</div>
)
}

return (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
)
})}
</>
);
}
Expand Down
3 changes: 1 addition & 2 deletions src/pages/schedule/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ import useLocalStorage from "../../hooks/useLocalStorage";
import useSetAnalyticsFlag from "../../hooks/useSetAnalyticsFlag";

export async function getServerSideProps(context) {

const session = await getSession(context) as CustomSession;
const currentTerms = await getAvailableTermsForSchedulePlanning();
const term = (context.query.term ?? currentTerms[0]) as string;
const term = (context?.query?.term ?? currentTerms[0]) as string;

const authorized = session?.user?.authorized ?? false;

Expand Down
Loading