Skip to content

Commit

Permalink
add login updates (#637)
Browse files Browse the repository at this point in the history
* add login updates

* add login updates

* edge case fixes
  • Loading branch information
jeromehardaway authored Nov 24, 2024
1 parent f6b398f commit 9cd9499
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 44 deletions.
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

0 comments on commit 9cd9499

Please sign in to comment.