1- import { useState , useEffect } from 'react' ;
1+ import { useState , useEffect , useCallback } from 'react' ;
22import { signIn , useSession } from 'next-auth/react' ;
33import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query' ;
44import axios from 'axios' ;
55import toast from 'react-hot-toast' ;
66import LoadingDots from '@/components/utils/loading-dots' ;
7- import GoogleIcon from '@/components/utils/google-icon' ;
8- import GitHubIcon from '@/components/utils/github-icon' ;
9- import DiscordIcon from '@/components/utils/discord-icon' ;
7+ import { FaDiscord , FaGithub , FaGoogle } from 'react-icons/fa' ;
8+ import React from 'react' ;
109import ReAuthModal from '@/components/shared/modals/reauth-modal' ;
1110import { CheckCircle , X , Link as LinkIcon } from 'lucide-react' ;
11+ import type { IconType } from 'react-icons' ;
1212
1313const AccountLinking = ( ) => {
1414 const { data : session } = useSession ( ) ;
@@ -18,24 +18,81 @@ const AccountLinking = () => {
1818 const [ providerToLink , setProviderToLink ] = useState ( null ) ;
1919
2020 // Fetch linked accounts
21- const { data : linkedAccounts , isLoading, refetch } = useQuery ( {
21+ const {
22+ data : linkedAccounts ,
23+ isLoading,
24+ refetch,
25+ } = useQuery ( {
2226 queryKey : [ 'linked-accounts' ] ,
2327 queryFn : async ( ) => {
2428 const response = await axios . get ( '/api/auth/linked-accounts' ) ;
2529 return response . data ;
2630 } ,
27- enabled : ! ! session ?. user ?. id ,
31+ enabled : ! ! session ?. user ,
2832 } ) ;
2933
34+ // Handle re-authentication completion
35+ const handleReAuthCompletion = useCallback ( async ( token , linkProvider ) => {
36+ try {
37+ console . log (
38+ 'Re-auth completed, will start linking in 2 seconds...' ,
39+ linkProvider
40+ ) ;
41+ setLoadingProvider ( linkProvider ) ;
42+
43+ // Add a delay to ensure the previous OAuth flow has fully completed
44+ // This prevents OAuth state conflicts between GitHub reauth and Discord linking
45+ setTimeout ( ( ) => {
46+ console . log ( 'Starting Discord OAuth flow for account linking' ) ;
47+ signIn ( linkProvider , {
48+ callbackUrl : `/admin/settings?tab=accounts&action=complete&token=${ token } ` ,
49+ } ) ;
50+ } , 2000 ) ; // 2 second delay to ensure OAuth state is cleared
51+ } catch ( error ) {
52+ console . error ( 'Error completing re-auth:' , error ) ;
53+ toast . error ( 'Failed to complete account linking' ) ;
54+ setLoadingProvider ( null ) ;
55+ }
56+ } , [ ] ) ;
57+
58+ // Handle account linking completion
59+ const handleLinkCompletion = useCallback (
60+ async ( token ) => {
61+ try {
62+ console . log ( 'Processing account link completion with token:' , token ) ;
63+
64+ const response = await axios . post ( '/api/auth/process-link' , { token } ) ;
65+
66+ if ( response . status === 200 ) {
67+ toast . success ( 'Account linked successfully!' ) ;
68+ refetch ( ) ; // Refresh the linked accounts list
69+ setLoadingProvider ( null ) ;
70+ }
71+ } catch ( error ) {
72+ console . error ( 'Error completing account link:' , error ) ;
73+ const message = axios . isAxiosError ( error )
74+ ? ( error . response ?. data as any ) ?. message
75+ : undefined ;
76+ toast . error ( message || 'Failed to link account' ) ;
77+ setLoadingProvider ( null ) ;
78+ }
79+ } ,
80+ [ refetch ]
81+ ) ;
82+
3083 // Check URL parameters and handle re-authentication flow
3184 useEffect ( ( ) => {
3285 const urlParams = new URLSearchParams ( window . location . search ) ;
3386 const action = urlParams . get ( 'action' ) ;
3487 const token = urlParams . get ( 'token' ) ;
3588 const linkProvider = urlParams . get ( 'linkProvider' ) ;
36-
37- console . log ( 'AccountLinking useEffect triggered with:' , { action, token, linkProvider } ) ;
38-
89+
90+ console . log ( 'AccountLinking useEffect triggered with:' , {
91+ action,
92+ token,
93+ linkProvider,
94+ } ) ;
95+
3996 if ( action === 'link' ) {
4097 // Refresh the linked accounts data after regular sign-in
4198 refetch ( ) ;
@@ -44,63 +101,26 @@ const AccountLinking = () => {
44101 window . history . replaceState ( { } , '' , '/admin/settings?tab=accounts' ) ;
45102 } else if ( action === 'reauth' && token && linkProvider ) {
46103 // Handle re-authentication completion
47- console . log ( 'Re-authentication completed, preparing to link:' , linkProvider ) ;
48-
104+ console . log (
105+ 'Re-authentication completed, preparing to link:' ,
106+ linkProvider
107+ ) ;
108+
49109 // Clear the URL parameters first to prevent re-triggering
50110 const newUrl = window . location . pathname + '?tab=accounts' ;
51111 window . history . replaceState ( { } , '' , newUrl ) ;
52-
112+
53113 // Add a delay and then trigger the Discord linking
54114 handleReAuthCompletion ( token , linkProvider ) ;
55115 } else if ( action === 'complete' && token ) {
56116 // Handle account linking completion
57117 console . log ( 'CALLING handleLinkCompletion with token:' , token ) ;
58118 handleLinkCompletion ( token ) ;
59-
119+
60120 // Clear URL immediately to prevent duplicate calls
61121 window . history . replaceState ( { } , '' , '/admin/settings?tab=accounts' ) ;
62122 }
63- } , [ ] ) ; // Remove refetch dependency to prevent infinite re-runs
64-
65- // Handle re-authentication completion
66- const handleReAuthCompletion = async ( token , linkProvider ) => {
67- try {
68- console . log ( 'Re-auth completed, will start linking in 2 seconds...' , linkProvider ) ;
69- setLoadingProvider ( linkProvider ) ;
70-
71- // Add a delay to ensure the previous OAuth flow has fully completed
72- // This prevents OAuth state conflicts between GitHub reauth and Discord linking
73- setTimeout ( ( ) => {
74- console . log ( 'Starting Discord OAuth flow for account linking' ) ;
75- signIn ( linkProvider , {
76- callbackUrl : `/admin/settings?tab=accounts&action=complete&token=${ token } ` ,
77- } ) ;
78- } , 2000 ) ; // 2 second delay to ensure OAuth state is cleared
79- } catch ( error ) {
80- console . error ( 'Error completing re-auth:' , error ) ;
81- toast . error ( 'Failed to complete account linking' ) ;
82- setLoadingProvider ( null ) ;
83- }
84- } ;
85-
86- // Handle account linking completion
87- const handleLinkCompletion = async ( token ) => {
88- try {
89- console . log ( 'Processing account link completion with token:' , token ) ;
90-
91- const response = await axios . post ( '/api/auth/process-link' , { token } ) ;
92-
93- if ( response . status === 200 ) {
94- toast . success ( 'Account linked successfully!' ) ;
95- refetch ( ) ; // Refresh the linked accounts list
96- setLoadingProvider ( null ) ;
97- }
98- } catch ( error ) {
99- console . error ( 'Error completing account link:' , error ) ;
100- toast . error ( error . response ?. data ?. message || 'Failed to link account' ) ;
101- setLoadingProvider ( null ) ;
102- }
103- } ;
123+ } , [ refetch , handleLinkCompletion , handleReAuthCompletion ] ) ;
104124
105125 // Unlink account mutation
106126 const unlinkMutation = useMutation ( {
@@ -111,31 +131,34 @@ const AccountLinking = () => {
111131 queryClient . invalidateQueries ( [ 'linked-accounts' ] ) ;
112132 toast . success ( 'Account unlinked successfully' ) ;
113133 } ,
114- onError : ( error ) => {
115- toast . error ( error . response ?. data ?. message || 'Failed to unlink account' ) ;
134+ onError : ( error : unknown ) => {
135+ const message = axios . isAxiosError ( error )
136+ ? ( error . response ?. data as any ) ?. message
137+ : undefined ;
138+ toast . error ( message || 'Failed to unlink account' ) ;
116139 } ,
117140 } ) ;
118141
119142 const providers = [
120143 {
121144 id : 'google' ,
122145 name : 'Google' ,
123- icon : GoogleIcon ,
146+ icon : FaGoogle ,
124147 color : 'border-red-500 text-red-500 hover:bg-red-50' ,
125148 } ,
126149 {
127150 id : 'github' ,
128151 name : 'GitHub' ,
129- icon : GitHubIcon ,
152+ icon : FaGithub ,
130153 color : 'border-gray-800 text-gray-800 hover:bg-gray-50' ,
131154 } ,
132155 {
133156 id : 'discord' ,
134157 name : 'Discord' ,
135- icon : DiscordIcon ,
158+ icon : FaDiscord ,
136159 color : 'border-indigo-600 text-indigo-600 hover:bg-indigo-50' ,
137160 } ,
138- ] ;
161+ ] as const ;
139162
140163 const handleLinkAccount = ( providerId ) => {
141164 setProviderToLink ( providerId ) ;
@@ -161,7 +184,7 @@ const AccountLinking = () => {
161184 } ;
162185
163186 const isLinked = ( providerId ) => {
164- return linkedAccounts ?. some ( account => account . provider === providerId ) ;
187+ return linkedAccounts ?. some ( ( account ) => account . provider === providerId ) ;
165188 } ;
166189
167190 if ( isLoading ) {
@@ -177,18 +200,22 @@ const AccountLinking = () => {
177200 < div className = "p-6 bg-white border rounded-2xl" >
178201 < div className = "flex items-center gap-3 mb-6" >
179202 < LinkIcon className = "w-5 h-5 text-gray-600" />
180- < h3 className = "text-lg font-semibold text-gray-900" > Linked Accounts</ h3 >
203+ < h3 className = "text-lg font-semibold text-gray-900" >
204+ Linked Accounts
205+ </ h3 >
181206 </ div >
182-
207+
183208 < p className = "mb-6 text-sm text-gray-600" >
184209 Link multiple accounts to sign in with any of them.
185210 </ p >
186211
187212 < div className = "space-y-4" >
188213 { providers . map ( ( provider ) => {
189214 const linked = isLinked ( provider . id ) ;
190- const IconComponent = provider . icon ;
191-
215+ const IconComponent = provider . icon as React . ComponentType <
216+ React . SVGProps < SVGSVGElement >
217+ > ;
218+
192219 return (
193220 < div
194221 key = { provider . id }
@@ -197,22 +224,24 @@ const AccountLinking = () => {
197224 < div className = "flex items-center gap-3" >
198225 < IconComponent />
199226 < div >
200- < h4 className = "font-medium text-gray-900" > { provider . name } </ h4 >
227+ < h4 className = "font-medium text-gray-900" >
228+ { provider . name }
229+ </ h4 >
201230 < p className = "text-sm text-gray-500" >
202231 { linked ? 'Connected' : 'Not connected' }
203232 </ p >
204233 </ div >
205234 </ div >
206235
207236 < div className = "flex items-center gap-2" >
208- { linked && (
209- < CheckCircle className = "w-5 h-5 text-green-500" />
210- ) }
211-
237+ { linked && < CheckCircle className = "w-5 h-5 text-green-500" /> }
238+
212239 { linked ? (
213240 < button
214241 onClick = { ( ) => handleUnlinkAccount ( provider . id ) }
215- disabled = { unlinkMutation . isLoading || linkedAccounts ?. length <= 1 }
242+ disabled = {
243+ unlinkMutation . isLoading || linkedAccounts ?. length <= 1
244+ }
216245 className = { `px-3 py-1.5 text-sm border rounded-md transition-colors ${
217246 linkedAccounts ?. length <= 1
218247 ? 'border-gray-200 text-gray-400 cursor-not-allowed'
@@ -253,15 +282,24 @@ const AccountLinking = () => {
253282 < div className = "p-4 mt-6 rounded-lg bg-blue-50" >
254283 < div className = "flex items-start gap-3" >
255284 < div className = "flex-shrink-0" >
256- < svg className = "w-5 h-5 text-blue-400" viewBox = "0 0 20 20" fill = "currentColor" >
257- < path fillRule = "evenodd" d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule = "evenodd" />
285+ < svg
286+ className = "w-5 h-5 text-blue-400"
287+ viewBox = "0 0 20 20"
288+ fill = "currentColor"
289+ >
290+ < path
291+ fillRule = "evenodd"
292+ d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
293+ clipRule = "evenodd"
294+ />
258295 </ svg >
259296 </ div >
260297 < div className = "flex-1 min-w-0 text-sm text-blue-700" >
261298 < p className = "font-medium" > Security First</ p >
262299 < p className = "mt-1" >
263- For your security, you'll need to re-authenticate with your original account
264- before linking any new accounts. This prevents unauthorized account linking.
300+ For your security, you'll need to re-authenticate with your
301+ original account before linking any new accounts. This prevents
302+ unauthorized account linking.
265303 </ p >
266304 </ div >
267305 </ div >
0 commit comments