Skip to content
Open
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
53 changes: 46 additions & 7 deletions app/api/auth/[...nextauth]/auth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import CredentialsProvider from 'next-auth/providers/credentials';
import { AuthService } from '@/services/auth.service';
import { AuthSharingService } from '@/services/auth-sharing.service';

export class NextAuthError extends Error {
constructor(
message: string,
public readonly code: string
) {
super(message);
this.name = 'NextAuthError';
}
}

// Debug flag - set to true to enable detailed authentication logging
const DEBUG_AUTH = true;

Expand Down Expand Up @@ -77,7 +87,7 @@ export const authOptions: NextAuthOptions = {
error: '/auth/error',
},
callbacks: {
async signIn({ user, account, profile }) {
async signIn({ user, account, profile, email, credentials }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need new props?

if (account?.type === 'oauth') {
try {
// Log OAuth token state for debugging
Expand Down Expand Up @@ -141,25 +151,54 @@ export const authOptions: NextAuthOptions = {

return true;
} catch (error) {
// Log detailed error information
const errorType = error instanceof Error ? error.message : 'AuthenticationFailed';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should standardize error handling and use consistent error classes (:144-150)
Good usage example in auth.service.ts and services/publication.service.ts (Lines 52-54, 69-76):

// Should be something like this:
const errorType = error instanceof Error ? error.message : 'AuthenticationFailed';
console.error('[Auth] Google OAuth error', {
error: errorType,
errorType: error instanceof Error ? error.constructor.name : typeof error,
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
throw new NextAuthError(errorType, 'OAUTH_ERROR');

// add this to the top after imports
export class NextAuthError extends Error {
constructor(
message: string,
public readonly code: string
) {
super(message);
this.name = 'NextAuthError';
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I agree!

console.error('[Auth] Google OAuth error', {
error: error instanceof Error ? error.message : 'Unknown error',
error: errorType,
errorType: error instanceof Error ? error.constructor.name : typeof error,
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});

// Preserve specific error messages for better debugging
if (error instanceof Error) {
throw new Error(error.message);
// Return false for OAuthAccountNotLinked to trigger consistent error flow
// This ensures the error is properly handled by the redirect callback
if (errorType === 'OAuthAccountNotLinked') {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the NextAuth documentation, the signIn callback can return a URL string to redirect users directly. This would eliminate the current redirect chain and provide a cleaner user experience.

Current approach:

  • Return false → NextAuth redirects to /auth/error
  • Error page redirects to /auth/signin with error param
  • Sign-in page processes error code and shows message

Proposed approach:

  • Return URL directly: /auth/signin?errorMessage=Please use email and password to login to your account.
  • Sign-in page reads errorMessage param and displays it
  • Remove errorMessage param from URL after rendering to clean up the URL

Benefits:

  • Eliminates unnecessary redirect chain
  • Cleaner, more maintainable code
  • Custom error messages without complex error code mapping

}
throw new Error('AuthenticationFailed');

throw new NextAuthError(errorType, 'OAUTH_ERROR');
}
}

return true;
},

async redirect({ url, baseUrl }) {
if (url.includes('/auth/error')) {
const urlObj = new URL(url, baseUrl);
const error = urlObj.searchParams.get('error');

// Map various OAuth error codes to OAuthAccountNotLinked
// This handles environment-specific error code variations
if (
error &&
(error === 'OAuthSignin' ||
error === 'OAuthCallback' ||
error === 'AccessDenied' ||
error === 'Callback' ||
error === 'OAuthCreateAccount' ||
error === 'AuthenticationFailed')
) {
urlObj.searchParams.set('error', 'OAuthAccountNotLinked');
}

return urlObj.toString();
}

if (url.startsWith('/')) return `${baseUrl}${url}`;
if (new URL(url).origin === baseUrl) return url;
return baseUrl;
},

async jwt({ token, user, account }) {
if (account && user) {
return {
Expand Down
18 changes: 14 additions & 4 deletions app/auth/error/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
'use client';

import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
import { useEffect, Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';

function ErrorContent() {
const searchParams = useSearchParams();
const error = searchParams?.get('error');
const router = useRouter();

return null; // This page won't render anything, it just redirects
useEffect(() => {
const error = searchParams?.get('error');
const callbackUrl = searchParams?.get('callbackUrl') || '/';

const params = new URLSearchParams({ callbackUrl });
if (error) params.set('error', error);

router.replace(`/auth/signin?${params}`);
}, [searchParams, router]);

return null;
}

export default function ErrorPage() {
Expand Down
45 changes: 30 additions & 15 deletions app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,40 @@

import { useSearchParams } from 'next/navigation';
import AuthContent from '@/components/Auth/AuthContent';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { Suspense } from 'react';

function SignInContent() {
const router = useRouter();
const searchParams = useSearchParams();
const error = searchParams?.get('error');
const callbackUrl = searchParams?.get('callbackUrl') || '/';
const errorCode = searchParams?.get('error');
let callbackUrl = searchParams?.get('callbackUrl') || '/';
//this is to help prevent redirecting issues when using the auth modal after an error occurs
if (callbackUrl.startsWith('http')) {
try {
const url = new URL(callbackUrl);
callbackUrl = url.pathname + url.search + url.hash;
} catch {}
}

if (callbackUrl.includes('/auth/')) {
callbackUrl = '/';
}

let error = null;
// Map various OAuth error codes to the account linking message
if (
errorCode === 'OAuthAccountNotLinked' ||
errorCode === 'OAuthSignin' ||
errorCode === 'OAuthCallback' ||
errorCode === 'AccessDenied' ||
errorCode === 'Callback' ||
errorCode === 'OAuthCreateAccount' ||
Comment on lines +27 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where are these errors specified?

errorCode === 'AuthenticationFailed'
) {
error = 'Enter email and password to login to your account.';
} else if (errorCode) {
error = 'An error occurred during authentication. Please try again.';
}

return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
Expand All @@ -35,17 +60,7 @@ function SignInContent() {
</div>

<div className="bg-white w-full max-w-md rounded-lg shadow-sm border border-gray-200 p-8">
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">{error}</p>
</div>
)}

<AuthContent
initialError={error}
onSuccess={() => router.push(callbackUrl)}
showHeader={false}
/>
<AuthContent initialError={error} callbackUrl={callbackUrl} showHeader={false} />
</div>

<div className="mt-8 text-center text-sm text-gray-500">
Expand Down
3 changes: 3 additions & 0 deletions components/Auth/AuthContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface AuthContentProps {
initialScreen?: AuthScreen;
showHeader?: boolean;
modalView?: boolean;
callbackUrl?: string;
}

export default function AuthContent({
Expand All @@ -23,6 +24,7 @@ export default function AuthContent({
initialScreen = 'SELECT_PROVIDER',
showHeader = true,
modalView = false,
callbackUrl,
}: AuthContentProps) {
const [screen, setScreen] = useState<AuthScreen>(initialScreen);
const [email, setEmail] = useState('');
Expand All @@ -48,6 +50,7 @@ export default function AuthContent({
setError,
showHeader,
modalView,
callbackUrl,
};

return (
Expand Down
33 changes: 15 additions & 18 deletions components/Auth/screens/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Props extends BaseScreenProps {
setIsLoading: (loading: boolean) => void;
onSuccess?: () => void;
modalView?: boolean;
callbackUrl?: string;
}

export default function Login({
Expand All @@ -27,6 +28,7 @@ export default function Login({
onBack,
onForgotPassword,
modalView = false,
callbackUrl,
}: Props) {
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
Expand All @@ -45,28 +47,23 @@ export default function Login({
setError(null);

try {
// This endpoint will return a CredentialsSignin error with no description.
// Currently we try to login with email and password + fetch the user's data separately,
// because the current endpoint only returns a token
// So, we show "Invalid email or password" error
const result = await signIn('credentials', {
email,
password,
redirect: false,
});

if (result?.error) {
setError('Invalid email or password');
if (callbackUrl) {
await signIn('credentials', { email, password, callbackUrl });
} else {
setIsRedirecting(true); // Set redirecting state before navigation
onSuccess?.();
onClose();
const result = await signIn('credentials', { email, password, redirect: false });

if (result?.error) {
setError('Invalid email or password');
} else {
setIsRedirecting(true);
onSuccess?.();
onClose();
}
}
} catch (err) {
} catch {
setError('Login failed');
} finally {
if (!isRedirecting) {
// Only reset loading if we're not redirecting
if (!isRedirecting && !callbackUrl) {
setIsLoading(false);
}
}
Expand Down
32 changes: 16 additions & 16 deletions components/Auth/screens/SelectProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ export default function SelectProvider({
const emailInputRef = useAutoFocus<HTMLInputElement>(true);
const { referralCode } = useReferral();

const getCallbackUrl = () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good refactor 🦾

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

const searchParams = new URLSearchParams(globalThis.location.search);
return searchParams.get('callbackUrl') || '/';
};

const initiateGoogleSignIn = (callbackUrl: string) => {
const finalUrl = referralCode
? new URL('/referral/join/apply-referral-code', globalThis.location.origin).toString() +
`?refr=${referralCode}&redirect=${encodeURIComponent(callbackUrl)}`
: callbackUrl;

signIn('google', { callbackUrl: finalUrl });
};

const handleCheckAccount = async (e?: React.FormEvent) => {
e?.preventDefault();
if (!isValidEmail(email)) {
Expand All @@ -48,7 +62,7 @@ export default function SelectProvider({

if (response.exists) {
if (response.auth === 'google') {
signIn('google', { callbackUrl: '/' });
initiateGoogleSignIn(getCallbackUrl());
} else if (response.is_verified) {
onContinue();
} else {
Expand All @@ -68,21 +82,7 @@ export default function SelectProvider({
AnalyticsService.logEvent(LogEvent.AUTH_VIA_GOOGLE_INITIATED).catch((error) => {
console.error('Analytics failed:', error);
});

const searchParams = new URLSearchParams(window.location.search);
const originalCallbackUrl = searchParams.get('callbackUrl') || '/';

let finalCallbackUrl = originalCallbackUrl;

if (referralCode) {
// Create referral application URL with referral code and redirect as URL parameters
const referralUrl = new URL('/referral/join/apply-referral-code', window.location.origin);
referralUrl.searchParams.set('refr', referralCode);
referralUrl.searchParams.set('redirect', originalCallbackUrl);
finalCallbackUrl = referralUrl.toString();
}

signIn('google', { callbackUrl: finalCallbackUrl });
initiateGoogleSignIn(getCallbackUrl());
};

return (
Expand Down
31 changes: 18 additions & 13 deletions services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,21 @@ export class AuthService {
try {
return await ApiClient.post<User>(`${this.BASE_PATH}/auth/register/`, credentials);
} catch (error: any) {
const { status } = error.message;
const data = error instanceof ApiError ? error.errors : {};
const errorMsg = Object.values(data as Record<string, string[]>)?.[0]?.[0];
throw new AuthError(errorMsg || 'Registration failed', status);
if (error instanceof ApiError) {
const data = error.errors || {};
const errorValues = Object.values(data as Record<string, string[]>)?.[0];
const errorMsg = Array.isArray(errorValues)
? errorValues[0]
: typeof errorValues === 'string'
? errorValues
: 'Registration failed';
throw new AuthError(errorMsg || 'Registration failed', error.status);
}

// Handle non-ApiError exceptions (network errors, etc.)
const errorMsg = error instanceof Error ? error.message : 'Registration failed';
console.error('[AuthService] Registration error:', error);
throw new AuthError(errorMsg, undefined);
}
}

Expand Down Expand Up @@ -171,16 +182,10 @@ export class AuthService {
timestamp: new Date().toISOString(),
});

switch (response.status) {
case 401:
throw new Error('AuthenticationFailed');
case 403:
throw new Error('AccessDenied');
case 409:
throw new Error('Verification');
default:
throw new Error('AuthenticationFailed');
if (response.status === 400 || response.status === 403 || response.status === 409) {
throw new Error('OAuthAccountNotLinked');
}
Comment on lines +185 to 187
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are doing wrong mapping here. because the error we are trying to handle is resulting with 400 error (When trying to register with an email that's already registered).

throw new Error('AuthenticationFailed');
}

const data = await response.json();
Expand Down