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
92 changes: 80 additions & 12 deletions src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@ import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import type { AuthOptions } from "next-auth";

// Utility function to handle GitHub API requests with timeout
const fetchWithTimeout = async (
url: string,
options: RequestInit,
timeout = 5000
): Promise<Response> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
};

export const authOptions: AuthOptions = {
providers: [
GithubProvider({
Expand All @@ -16,52 +38,98 @@ export const authOptions: AuthOptions = {
],
callbacks: {
async signIn({ account }) {
if (account?.provider === "github") {
if (account?.provider === "github" && account.access_token) {
try {
const res = await fetch("https://api.github.com/user/orgs", {
headers: {
Authorization: `Bearer ${account.access_token}`,
// Add timeout protection to the GitHub API call
const res = await fetchWithTimeout(
"https://api.github.com/user/orgs",
{
headers: {
Accept: "application/vnd.github.v3+json",
Authorization: `Bearer ${account.access_token}`,
"User-Agent": "NextAuth.js",
},
},
});
5000 // 5 second timeout
);

// Handle rate limiting
if (res.status === 403) {
console.error("GitHub API rate limit exceeded");
return false;
}

if (!res.ok) {
console.error(`GitHub API error: ${res.status}`);
return false;
}

const orgs = await res.json();

if (!Array.isArray(orgs)) {
console.error("Invalid response from GitHub API");
return false;
}

const isMember = orgs.some((org) => org.login === process.env.GITHUB_ORG);
const githubOrg = process.env.GITHUB_ORG;
if (!githubOrg) {
console.error("GITHUB_ORG environment variable not set");
return false;
}

const isMember = orgs.some((org) => org.login === githubOrg);

if (!isMember) {
console.error("User is not a member of the required organization");
return false;
}

return true;
} catch (error) {
if (error instanceof Error) {
if (error.name === "AbortError") {
console.error("GitHub API request timed out");
} else {
console.error("Error checking GitHub organization membership:", error);
}
}
return false;
}
}

return true;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
try {
if (session.user) {
session.user.id = token.id as string;
}
return session;
} catch (error) {
console.error("Error in session callback:", error);
return session;
}
return session;
},
async jwt({ token, user, account }) {
if (account && user) {
token.id = user.id;
try {
if (account && user) {
token.id = user.id;
}
return token;
} catch (error) {
console.error("Error in JWT callback:", error);
return token;
}
return token;
},
},
pages: {
error: "/auth/error", // Add this if you want to handle auth errors with a custom page
},
secret: process.env.NEXTAUTH_SECRET,
debug: process.env.NODE_ENV === "development",
};

// Type augmentation for next-auth
declare module "next-auth" {
interface Session {
user: {
Expand Down
158 changes: 158 additions & 0 deletions src/pages/api/auth/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import type { NextPage } from "next";
import Layout from "@layout/layout-01";

type ErrorType = {
[key: string]: {
title: string;
message: string;
action: string;
};
};

const errorMessages: ErrorType = {
default: {
title: "Authentication Error",
message:
"An unexpected error occurred during the authentication process.",
action: "Please try signing in again.",
},
configuration: {
title: "Server Configuration Error",
message: "There is a problem with the server configuration.",
action: "Please contact support for assistance.",
},
accessdenied: {
title: "Access Denied",
message:
"You must be a member of the required GitHub organization to access this application.",
action: "Please request access from your organization administrator.",
},
verification: {
title: "Account Verification Required",
message: "Your account requires verification before continuing.",
action: "Please check your email for verification instructions.",
},
signin: {
title: "Sign In Error",
message: "The sign in attempt was unsuccessful.",
action: "Please try again or use a different method to sign in.",
},
callback: {
title: "Callback Error",
message: "There was a problem with the authentication callback.",
action: "Please try signing in again. If the problem persists, clear your browser cookies.",
},
oauthsignin: {
title: "GitHub Sign In Error",
message: "Unable to initiate GitHub sign in process.",
action: "Please try again or check if GitHub is accessible.",
},
oauthcallback: {
title: "GitHub Callback Error",
message: "There was a problem processing the GitHub authentication.",
action: "Please try signing in again or ensure you've granted the required permissions.",
},
};

type PageWithLayout = NextPage & {
Layout?: typeof Layout;
};

const AuthError: PageWithLayout = () => {
const router = useRouter();
const [error, setError] = useState(errorMessages.default);
const [countdown, setCountdown] = useState(10);

useEffect(() => {
const errorType = router.query.error as string;
if (errorType && errorMessages[errorType]) {
setError(errorMessages[errorType]);
}
}, [router.query]);

useEffect(() => {
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
router.push("/").catch(console.error);
return 0;
}
return prev - 1;
});
}, 1000);

return () => clearInterval(timer);
}, [router]);

return (
<div className="tw-min-h-screen tw-bg-gray-50 tw-flex tw-flex-col tw-justify-center tw-py-12 tw-sm:px-6 tw-lg:px-8">
<div className="tw-sm:mx-auto tw-sm:w-full tw-sm:max-w-md">
<div className="tw-bg-white tw-py-8 tw-px-4 tw-shadow tw-sm:rounded-lg tw-sm:px-10">
<div className="tw-text-center">
<h2 className="tw-text-2xl tw-font-bold tw-text-gray-900 tw-mb-4">
{error.title}
</h2>
<div className="tw-rounded-md tw-bg-red-50 tw-p-4 tw-mb-6">
<div className="tw-flex">
<div className="tw-flex-shrink-0">
<svg
className="tw-h-5 tw-w-5 tw-text-red-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="tw-ml-3">
<p className="tw-text-sm tw-text-red-700">
{error.message}
</p>
<p className="tw-mt-2 tw-text-sm tw-text-red-700">
{error.action}
</p>
</div>
</div>
</div>

<div className="tw-space-y-4">
<Link
href="/"
className="tw-inline-flex tw-items-center tw-px-4 tw-py-2 tw-border tw-border-transparent tw-text-sm tw-font-medium tw-rounded-md tw-shadow-sm tw-text-white tw-bg-primary tw-hover:tw-bg-opacity-90 tw-focus:tw-outline-none tw-focus:tw-ring-2 tw-focus:tw-ring-offset-2 tw-focus:tw-ring-primary"
>
Return to Home Page
</Link>

<p className="tw-text-sm tw-text-gray-500">
Redirecting in {countdown} seconds...
</p>

<div className="tw-mt-4 tw-text-sm tw-text-gray-500">
Need help?{" "}
<a
href="mailto:[email protected]"
className="tw-font-medium tw-text-primary tw-hover:tw-text-opacity-90"
>
Contact Support
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

AuthError.Layout = Layout;

export default AuthError;
Loading
Loading