diff --git a/app/api/auth/[...nextauth]/auth.config.ts b/app/api/auth/[...nextauth]/auth.config.ts
index 06b80ef1a..eceacb260 100644
--- a/app/api/auth/[...nextauth]/auth.config.ts
+++ b/app/api/auth/[...nextauth]/auth.config.ts
@@ -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;
@@ -77,7 +87,7 @@ export const authOptions: NextAuthOptions = {
error: '/auth/error',
},
callbacks: {
- async signIn({ user, account, profile }) {
+ async signIn({ user, account, profile, email, credentials }) {
if (account?.type === 'oauth') {
try {
// Log OAuth token state for debugging
@@ -141,25 +151,54 @@ export const authOptions: NextAuthOptions = {
return true;
} catch (error) {
- // Log detailed error information
+ const errorType = error instanceof Error ? error.message : 'AuthenticationFailed';
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;
}
- 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 {
diff --git a/app/auth/error/page.tsx b/app/auth/error/page.tsx
index 8f6169abd..8601696e8 100644
--- a/app/auth/error/page.tsx
+++ b/app/auth/error/page.tsx
@@ -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() {
diff --git a/app/auth/signin/page.tsx b/app/auth/signin/page.tsx
index db6662475..fd175a85c 100644
--- a/app/auth/signin/page.tsx
+++ b/app/auth/signin/page.tsx
@@ -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' ||
+ 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 (
@@ -35,17 +60,7 @@ function SignInContent() {
- {error && (
-
- )}
-
-
router.push(callbackUrl)}
- showHeader={false}
- />
+
diff --git a/components/Auth/AuthContent.tsx b/components/Auth/AuthContent.tsx
index da79bc052..a8c9d9891 100644
--- a/components/Auth/AuthContent.tsx
+++ b/components/Auth/AuthContent.tsx
@@ -14,6 +14,7 @@ interface AuthContentProps {
initialScreen?: AuthScreen;
showHeader?: boolean;
modalView?: boolean;
+ callbackUrl?: string;
}
export default function AuthContent({
@@ -23,6 +24,7 @@ export default function AuthContent({
initialScreen = 'SELECT_PROVIDER',
showHeader = true,
modalView = false,
+ callbackUrl,
}: AuthContentProps) {
const [screen, setScreen] = useState
(initialScreen);
const [email, setEmail] = useState('');
@@ -48,6 +50,7 @@ export default function AuthContent({
setError,
showHeader,
modalView,
+ callbackUrl,
};
return (
diff --git a/components/Auth/screens/Login.tsx b/components/Auth/screens/Login.tsx
index aa3695b53..a71b8b7e7 100644
--- a/components/Auth/screens/Login.tsx
+++ b/components/Auth/screens/Login.tsx
@@ -14,6 +14,7 @@ interface Props extends BaseScreenProps {
setIsLoading: (loading: boolean) => void;
onSuccess?: () => void;
modalView?: boolean;
+ callbackUrl?: string;
}
export default function Login({
@@ -27,6 +28,7 @@ export default function Login({
onBack,
onForgotPassword,
modalView = false,
+ callbackUrl,
}: Props) {
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
@@ -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);
}
}
diff --git a/components/Auth/screens/SelectProvider.tsx b/components/Auth/screens/SelectProvider.tsx
index a61310a5e..04d3fbd60 100644
--- a/components/Auth/screens/SelectProvider.tsx
+++ b/components/Auth/screens/SelectProvider.tsx
@@ -33,6 +33,20 @@ export default function SelectProvider({
const emailInputRef = useAutoFocus(true);
const { referralCode } = useReferral();
+ const getCallbackUrl = () => {
+ 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)) {
@@ -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 {
@@ -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 (
diff --git a/services/auth.service.ts b/services/auth.service.ts
index 03b08f3f9..e3923f822 100644
--- a/services/auth.service.ts
+++ b/services/auth.service.ts
@@ -40,10 +40,21 @@ export class AuthService {
try {
return await ApiClient.post(`${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)?.[0]?.[0];
- throw new AuthError(errorMsg || 'Registration failed', status);
+ if (error instanceof ApiError) {
+ const data = error.errors || {};
+ const errorValues = Object.values(data as Record)?.[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);
}
}
@@ -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');
}
+ throw new Error('AuthenticationFailed');
}
const data = await response.json();