11"use client" ;
2+
23import { User } from "@supabase/supabase-js" ;
34import { useRouter , useSearchParams } from "next/navigation" ;
4- import { useEffect , useState } from "react" ;
5+ import { useEffect , useState , useCallback } from "react" ;
56import { Subscription } from "@/types/subscription" ;
67import { toast } from "sonner" ;
78import ProfileCard from "@/components/dashboard/profile-card" ;
89import SubscriptionCard from "@/components/dashboard/subscription-card" ;
910import FreeTrialCard from "@/components/dashboard/freetrial-card" ;
11+ import { isAllowedUrl } from "@/lib/utils" ;
12+ import { UnsafeUrlError } from "@/types/url" ;
1013
1114type DashboardPageProps = {
1215 subscription : Subscription | null ;
13- openAppQueryParams : string ;
16+ openAppQueryParams : string | URLSearchParams ;
1417 user : User ;
1518} ;
1619
@@ -30,54 +33,92 @@ export default function DashboardPage({
3033 percent_credit_used : null ,
3134 } ) ;
3235
33- useEffect ( ( ) => {
34- const handleCallbackForApp = async ( ) => {
35- // Handle callback
36- const callback = searchParams ?. get ( "callback" ) ;
37- if ( callback ) {
38- const decodedCallback = decodeURIComponent ( callback ) ;
39- const callbackUrl = new URL ( decodedCallback ) ;
40- const newSearchParams = new URLSearchParams ( callbackUrl . search ) ;
41- const openAppParams = new URLSearchParams ( openAppQueryParams ) ;
42-
43- openAppParams . forEach ( ( value , key ) => {
44- newSearchParams . append ( key , value ) ;
45- } ) ;
46-
47- callbackUrl . search = newSearchParams . toString ( ) ;
48- const openAppUrl = callbackUrl . toString ( ) ;
49-
50- router . push ( openAppUrl ) ;
51-
52- const currentUrl = new URL ( window . location . href ) ;
53- currentUrl . searchParams . delete ( "callback" ) ;
54- window . history . replaceState ( { } , "" , currentUrl . toString ( ) ) ;
36+ const handleCallbackForApp = useCallback ( async ( ) => {
37+ const callback = searchParams ?. get ( "callback" ) ;
38+ if ( ! callback ) return ;
39+
40+ try {
41+ const decodedCallback = decodeURIComponent ( callback ) ;
42+ const callbackUrl = new URL ( decodedCallback ) ;
43+
44+ if ( ! isAllowedUrl ( callbackUrl ) ) {
45+ throw new UnsafeUrlError ( decodedCallback ) ;
46+ }
47+
48+ const newSearchParams = new URLSearchParams ( callbackUrl . search ) ;
49+ const openAppParams =
50+ typeof openAppQueryParams === "string"
51+ ? new URLSearchParams ( openAppQueryParams )
52+ : openAppQueryParams ;
53+
54+ openAppParams . forEach ( ( value , key ) => {
55+ newSearchParams . append ( key , value ) ;
56+ } ) ;
57+
58+ callbackUrl . search = newSearchParams . toString ( ) ;
59+ const openAppUrl = callbackUrl . toString ( ) ;
60+
61+ // Double-check the final URL
62+ const finalUrl = new URL ( openAppUrl ) ;
63+ if ( ! isAllowedUrl ( finalUrl ) ) {
64+ throw new UnsafeUrlError ( openAppUrl ) ;
5565 }
56- } ;
5766
58- const getUserRequestsUsage = async ( ) => {
67+ router . push ( openAppUrl ) ;
68+
69+ const currentUrl = new URL ( window . location . href ) ;
70+ currentUrl . searchParams . delete ( "callback" ) ;
71+ window . history . replaceState ( { } , "" , currentUrl . toString ( ) ) ;
72+ } catch ( error ) {
73+ if ( error instanceof UnsafeUrlError ) {
74+ console . error ( error . message ) ;
75+ toast . error (
76+ "Unsafe link detected. Navigation blocked for your security." ,
77+ ) ;
78+ } else {
79+ console . error ( "Error in handleCallbackForApp:" , error ) ;
80+ toast . error (
81+ "An error occurred while processing the link. Please try again." ,
82+ ) ;
83+ }
84+ }
85+ } , [ router , searchParams , openAppQueryParams ] ) ;
86+
87+ const getUserRequestsUsage = useCallback ( async ( ) => {
88+ try {
89+ const response = await fetch ( "/api/dashboard-usage" , {
90+ method : "GET" ,
91+ headers : { "Content-Type" : "application/json" } ,
92+ } ) ;
93+
94+ if ( ! response . ok ) {
95+ throw new Error ( "Failed to fetch requests usage." ) ;
96+ }
97+ const usageData : UsageType = await response . json ( ) ;
98+ setUsage ( usageData ) ;
99+ } catch ( error ) {
100+ console . error ( "Error fetching requests usage:" , error ) ;
101+ toast . error ( "Failed to fetch usage data. Please try again later." ) ;
102+ } finally {
103+ setLoading ( false ) ;
104+ }
105+ } , [ ] ) ;
106+
107+ useEffect ( ( ) => {
108+ const runEffects = async ( ) => {
59109 try {
60- const response = await fetch ( "/api/dashboard-usage" , {
61- method : "GET" ,
62- headers : { "Content-Type" : "application/json" } ,
63- } ) ;
64-
65- if ( ! response . ok ) {
66- toast . error ( "Failed to fetch requests usage." ) ;
67- return ;
68- }
69- const usage = await response . json ( ) ;
70- setUsage ( usage ) ;
110+ await handleCallbackForApp ( ) ;
111+ await getUserRequestsUsage ( ) ;
71112 } catch ( error ) {
72- toast . error ( `Error fetching requests usage: ${ error } ` ) ;
73- } finally {
74- setLoading ( false ) ;
113+ console . error ( "Error in effect:" , error ) ;
114+ toast . error (
115+ "An unexpected error occurred. Please try refreshing the page." ,
116+ ) ;
75117 }
76118 } ;
77119
78- handleCallbackForApp ( ) ;
79- getUserRequestsUsage ( ) ;
80- } , [ router , searchParams , openAppQueryParams ] ) ;
120+ runEffects ( ) ;
121+ } , [ handleCallbackForApp , getUserRequestsUsage ] ) ;
81122
82123 return (
83124 < section className = "relative" >
@@ -91,7 +132,6 @@ export default function DashboardPage({
91132 </ div >
92133 < div className = "grid gap-6 lg:grid-cols-2" >
93134 < ProfileCard user = { user } />
94- { /* Below commented out until we implement Free Trial */ }
95135 { subscription ? (
96136 < SubscriptionCard
97137 subscription = { subscription }
0 commit comments