-
Couldn't load subscription status.
- Fork 14
Login Fixes #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Login Fixes #475
Changes from all commits
60396dc
e83877f
f07d92d
329b516
4601594
d24e26d
801690b
92958cd
e6ea8bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should standardize error handling and use consistent error classes (:144-150) // Should be something like this: // add this to the top after imports There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Proposed approach:
Benefits:
|
||
| } | ||
| 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 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"> | ||
|
|
@@ -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"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,20 @@ export default function SelectProvider({ | |
| const emailInputRef = useAutoFocus<HTMLInputElement>(true); | ||
| const { referralCode } = useReferral(); | ||
|
|
||
| const getCallbackUrl = () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good refactor 🦾 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) { | ||
|
|
@@ -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 ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
|
||
There was a problem hiding this comment.
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?