diff --git a/src/atoms/editor/index.ts b/src/atoms/editor/index.ts index 335ab48..abb2282 100644 --- a/src/atoms/editor/index.ts +++ b/src/atoms/editor/index.ts @@ -46,6 +46,11 @@ export const previewAccessTokenState = atom({ default: "", }); +export const previewRefreshTokenState = atom({ + key: "previewRefreshTokenState", + default: "", +}); + export const lastDashboardMetadata = atom({ key: "lastDashboardMetadata", default: null, diff --git a/src/components/editor/auth/AuthDrawer.tsx b/src/components/editor/auth/AuthDrawer.tsx index 7ffdcc2..10653ea 100644 --- a/src/components/editor/auth/AuthDrawer.tsx +++ b/src/components/editor/auth/AuthDrawer.tsx @@ -1,10 +1,13 @@ -import { isAuthDrawerOpen, previewAccessTokenState } from "@/atoms/editor"; +import { + isAuthDrawerOpen, + previewAccessTokenState, + previewRefreshTokenState, +} from "@/atoms/editor"; import { Accordion, AccordionItem, Divider, Input, - semanticColors, Switch, } from "@nextui-org/react"; import { motion } from "framer-motion"; @@ -22,16 +25,22 @@ const AuthDrawer = () => { const { dashboard, updateDashboard } = useDashboard(); const { theme } = useTheme(); const setPreviewAccessToken = useSetRecoilState(previewAccessTokenState); + const setPreviewRefreshToken = useSetRecoilState(previewRefreshTokenState); const { enabled, loginQueryData, + refreshQueryData, accessTokenField, + refreshTokenField, + refreshResponseTokenField, passwordInputLabel, userInputLabel, userQueryParameter, passwordQueryParameter, previewAccessToken, + previewRefreshToken, + refreshQueryParameter, title, buttonText, buttonColor, @@ -125,6 +134,18 @@ const AuthDrawer = () => { }} isDisabled={!enabled} /> + { + updateAuth({ + refreshTokenField: value, + }); + }} + isDisabled={!enabled} + /> { /> + +
+ { + updateAuth({ + refreshQueryData: { + queryId: refreshQuery.id, + connectionId: refreshQuery.connection_id, + method: refreshQuery.metadata.method, + }, + }); + }} + label={"Refresh Query"} + placeholder={"Select refresh query"} + isDisabled={!enabled} + type={QueryType.UPDATE} + /> + { + updateAuth({ + refreshResponseTokenField: value, + }); + }} + isDisabled={!enabled} + /> + { + setPreviewRefreshToken(value); + updateAuth({ + previewRefreshToken: value, + }); + }} + isDisabled={!enabled} + /> + { + updateAuth({ + refreshQueryParameter: value, + }); + }} + label={"Refresh Query Parameter"} + disabledKeys={[refreshQueryParameter || ""]} + isDisabled={!enabled} + /> +
+
{ const { theme } = useTheme(); const { accessToken, updateAccessToken } = useAccessToken({ dashboardId }); + const { updateDashboard } = useDashboard(); + + const { updateRefreshToken } = useRefreshToken({ dashboardId }); const isAuthOpen = useRecoilValue(isAuthDrawerOpen); + const updateAuth = (auth: any) => { + updateDashboard((prev) => ({ + ...prev, + metadata: { + ...prev.metadata, + auth: { + ...prev.metadata.auth, + ...auth, + }, + }, + })); + }; + const { execute, isPending } = useExecuteQuery({ dashboardId, onError: (error) => { @@ -50,8 +68,28 @@ const AuthVerifier = ({ if (auth && response.body[auth?.accessTokenField] !== undefined) { if (mode === "editor") { toast.success("Logged in successfully"); + if ( + auth?.refreshTokenField && + response.body[auth?.refreshTokenField] !== undefined + ) { + updateAuth({ + previewAccessToken: response.body[auth?.accessTokenField], + previewRefreshToken: response.body[auth?.refreshTokenField], + }); + } else { + updateAuth({ + previewAccessToken: response.body[auth?.accessTokenField], + }); + } } else { updateAccessToken(response.body[auth?.accessTokenField]); + + if ( + auth?.refreshTokenField && + response.body[auth?.refreshTokenField] !== undefined + ) { + updateRefreshToken(response.body[auth?.refreshTokenField]); + } } } else { toast.error("No access token found in response"); diff --git a/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx b/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx index a6aeac8..776d5dc 100644 --- a/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx +++ b/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx @@ -1,9 +1,13 @@ import { Icon } from "@/components/shared/IconPicker"; import useNavigation from "@/hooks/useNavigation"; import { SidebarProperties } from "@/types/editor/sidebar-types"; -import { extendVariants, Tab, Tabs } from "@nextui-org/react"; +import { Button, extendVariants, Tab, Tabs } from "@nextui-org/react"; import { useTheme } from "next-themes"; import { Key } from "react"; +import { Logout } from "iconsax-react"; +import { useParams } from "next/navigation"; +import useDashboard from "@/hooks/dashboards/useDashboard"; +import { isPublishPage, isPreviewPage } from "@/lib/helpers"; export default function FastboardSidebar({ properties, @@ -12,16 +16,29 @@ export default function FastboardSidebar({ }) { const { theme } = useTheme(); const { currentPage, changePage } = useNavigation(); - const { menuItems, backgroundColor, textColor, selectedColor } = properties; - const cursorClassName = `bg-[${selectedColor.light}]`; + + const { id: dashboardId } = useParams(); + + const { dashboard } = useDashboard(isPublishPage() ? "published" : "editor"); + + const { menuItems, backgroundColor, textColor } = properties; function handleSelectionChange(key: Key) { changePage(key.toString()); } + const handleLogout = () => { + localStorage.removeItem(`auth-${dashboardId}`); + localStorage.removeItem(`refresh-${dashboardId}`); + + window.location.reload(); + }; + + const isEditorPage = !isPreviewPage() && !isPublishPage(); + return (
))} + {/* logout button*/} + {dashboard?.metadata?.auth?.enabled && !isEditorPage && ( + + )}
); diff --git a/src/components/shared/Viewport.tsx b/src/components/shared/Viewport.tsx index bfce694..2daac21 100644 --- a/src/components/shared/Viewport.tsx +++ b/src/components/shared/Viewport.tsx @@ -2,7 +2,10 @@ import useDashboard from "@/hooks/dashboards/useDashboard"; import AuthVerifier from "../editor/auth/AuthVerifier"; import FastboardComponent from "../editor/fastboard-components/FastboardComponent"; import { useSetRecoilState } from "recoil"; -import { previewAccessTokenState } from "@/atoms/editor"; +import { + previewAccessTokenState, + previewRefreshTokenState, +} from "@/atoms/editor"; import useNavigation from "@/hooks/useNavigation"; import { useEffect } from "react"; import { getLayout } from "../editor/fastboard-components/utils"; @@ -16,15 +19,20 @@ export default function Viewport({ mode: "editor" | "preview" | "published"; }) { const { dashboard, loading, isError, error, getComponent } = useDashboard( - mode === "editor" || mode === "preview" ? "editor" : "published" + mode === "editor" || mode === "preview" ? "editor" : "published", ); const setPreviewAccessToken = useSetRecoilState(previewAccessTokenState); + const setPreviewRefreshToken = useSetRecoilState(previewRefreshTokenState); const { currentPage } = useNavigation(); useEffect(() => { if (mode === "editor" && dashboard?.metadata?.auth?.previewAccessToken) { setPreviewAccessToken(dashboard.metadata.auth.previewAccessToken); } + + if (mode === "editor" && dashboard?.metadata?.auth?.previewRefreshToken) { + setPreviewRefreshToken(dashboard.metadata.auth.previewRefreshToken); + } }, [dashboard]); const sidebarVisible = dashboard?.metadata?.sidebar?.visible ?? false; @@ -34,7 +42,7 @@ export default function Viewport({ : "100%"; const layoutsHeight = isHeaderVisible ? "90%" : "100%"; const header = getComponent( - dashboard?.metadata?.header?.componentId as string + dashboard?.metadata?.header?.componentId as string, ); const sidebar = dashboard?.metadata?.sidebar?.id ? getComponent(dashboard.metadata.sidebar?.id) @@ -109,8 +117,8 @@ export default function Viewport({ layout, currentPage, index, - mode === "editor" ? "editable" : "view" - ) + mode === "editor" ? "editable" : "view", + ), )} diff --git a/src/hooks/adapter/useExecuteQuery.ts b/src/hooks/adapter/useExecuteQuery.ts index 5e36b56..ca4c9f7 100644 --- a/src/hooks/adapter/useExecuteQuery.ts +++ b/src/hooks/adapter/useExecuteQuery.ts @@ -4,8 +4,14 @@ import { queryClient } from "@/app/providers"; import { adapterService } from "@/lib/services/adapter"; import { useState } from "react"; import { useRecoilValue } from "recoil"; -import { previewAccessTokenState } from "@/atoms/editor"; +import { + previewAccessTokenState, + previewRefreshTokenState, +} from "@/atoms/editor"; import { AxiosRequestConfig } from "axios"; +import { DashboardAuth } from "@/types/editor"; +import useDashboard from "@/hooks/dashboards/useDashboard"; +import { isPublishPage } from "@/lib/helpers"; const useExecuteQuery = ({ onSuccess, @@ -17,6 +23,10 @@ const useExecuteQuery = ({ dashboardId: string; }) => { const previewAccessToken = useRecoilValue(previewAccessTokenState); + const previewRefreshToken = useRecoilValue(previewRefreshTokenState); + + const { dashboard } = useDashboard(isPublishPage() ? "published" : "editor"); + const [invalidateQueries, setInvalidateQueries] = useState(); const { @@ -46,6 +56,8 @@ const useExecuteQuery = ({ parameters, previewAccessToken, config, + previewRefreshToken, + dashboard?.metadata?.auth as DashboardAuth | null, ); }, onSuccess: (data, variables) => { diff --git a/src/hooks/useData.ts b/src/hooks/useData.ts index cb845c7..7388e95 100644 --- a/src/hooks/useData.ts +++ b/src/hooks/useData.ts @@ -3,13 +3,23 @@ import { ContentType, QueryData } from "@/types/connections"; import { adapterService } from "@/lib/services/adapter"; import { useMemo, useState } from "react"; import { useRecoilValue } from "recoil"; -import { previewAccessTokenState } from "@/atoms/editor"; +import { + previewAccessTokenState, + previewRefreshTokenState, +} from "@/atoms/editor"; import { useParams } from "next/navigation"; +import useDashboard from "@/hooks/dashboards/useDashboard"; +import { isPublishPage } from "@/lib/helpers"; +import { DashboardAuth } from "@/types/editor"; const useData = (componentId: string, queryData: QueryData | null) => { const { queryId, connectionId } = queryData || {}; const { id: dashboardId } = useParams(); const previewAccessToken = useRecoilValue(previewAccessTokenState); + const previewRefreshToken = useRecoilValue(previewRefreshTokenState); + + const { dashboard } = useDashboard(isPublishPage() ? "published" : "editor"); + const { data, refetch, isLoading, isFetching, isError, error } = useQuery({ queryKey: ["get_data", connectionId, { queryId, componentId }], queryFn: () => fetchData(queryData), @@ -42,7 +52,9 @@ const useData = (componentId: string, queryData: QueryData | null) => { headers: { "Content-Type": contentType || ContentType.JSON, }, - } + }, + previewRefreshToken, + dashboard?.metadata?.auth as DashboardAuth | undefined, ); let responseData = response?.body; diff --git a/src/hooks/useRefreshToken.ts b/src/hooks/useRefreshToken.ts new file mode 100644 index 0000000..0af924b --- /dev/null +++ b/src/hooks/useRefreshToken.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; + +export const useRefreshToken = ({ dashboardId }: { dashboardId: string }) => { + const [refreshToken, setRefreshToken] = useState(null); + + useEffect(() => { + const token = localStorage.getItem(`auth-${dashboardId}`); + setRefreshToken(token); + + const listener = (e: StorageEvent) => { + if (e.key === `refresh-${dashboardId}`) { + setRefreshToken(e.newValue); + } + }; + + window.addEventListener("storage", listener); + + return () => { + window.removeEventListener("storage", listener); + }; + }, [dashboardId]); + + const updateRefreshToken = (token: string) => { + setRefreshToken(token); + localStorage.setItem(`refresh-${dashboardId}`, token); + }; + + return { refreshToken, updateRefreshToken }; +}; diff --git a/src/lib/services/adapter.ts b/src/lib/services/adapter.ts index 5601fe8..8ce5e98 100644 --- a/src/lib/services/adapter.ts +++ b/src/lib/services/adapter.ts @@ -10,6 +10,7 @@ import { isPreviewPage, isPublishPage } from "@/lib/helpers"; import { mapQuery } from "@/lib/services/connections"; import { AxiosRequestConfig } from "axios"; import { toBase64 } from "@/lib/file"; +import { DashboardAuth } from "@/types/editor"; const previewQuery = async ( connectionId: string, @@ -66,6 +67,9 @@ async function executeQuery( parameters?: Record, previewAccessToken?: string, config?: AxiosRequestConfig, + previewRefreshToken?: string, + dashboardAuth?: DashboardAuth | null, + shouldRefresh = true, ) { try { if (!queryId) { @@ -73,6 +77,7 @@ async function executeQuery( } const token = localStorage.getItem(`auth-${dashboardId}`); + const refreshToken = localStorage.getItem(`refresh-${dashboardId}`); const parametersToSend = parameters ?? {}; @@ -80,6 +85,8 @@ async function executeQuery( parametersToSend.token = viewMode ? token : previewAccessToken; + const refreshTokenToSend = viewMode ? refreshToken : previewRefreshToken; + const response = await axiosInstance.post( `/adapter/execute/${queryId}`, { @@ -90,7 +97,46 @@ async function executeQuery( if (response?.data.status_code && response?.data.status_code !== 200) { if (response?.data?.status_code === 401) { + if (refreshTokenToSend && dashboardAuth && shouldRefresh) { + const refreshQueryId = dashboardAuth.refreshQueryData?.queryId; + const refreshParameter = dashboardAuth.refreshQueryParameter; + const refreshResponse = await axiosInstance.post( + `/adapter/execute/${refreshQueryId}`, + { + parameters: { + [refreshParameter]: refreshTokenToSend, + }, + }, + ); + + if ( + refreshResponse?.data?.body?.[ + dashboardAuth.refreshResponseTokenField + ] + ) { + const newAccessToken = + refreshResponse.data.body[ + dashboardAuth.refreshResponseTokenField + ]; + + localStorage.setItem(`auth-${dashboardId}`, newAccessToken); + + return await executeQuery( + queryId, + dashboardId, + parameters, + newAccessToken, + config, + previewRefreshToken, + dashboardAuth, + false, // Prevent further refresh attempts + ); + } + } + localStorage.removeItem(`auth-${dashboardId}`); + localStorage.removeItem(`refresh-${dashboardId}`); + if (viewMode) { window.location.reload(); } diff --git a/src/types/editor/index.ts b/src/types/editor/index.ts index 426b09c..55ef3b7 100644 --- a/src/types/editor/index.ts +++ b/src/types/editor/index.ts @@ -41,12 +41,17 @@ export interface ModalFrame { export class DashboardAuth { enabled: boolean = false; loginQueryData: QueryData | null = null; + refreshQueryData: QueryData | null = null; accessTokenField: string = ""; + refreshTokenField: string = ""; + refreshResponseTokenField: string = ""; userInputLabel: string = "username"; passwordInputLabel: string = "password"; userQueryParameter: string = ""; passwordQueryParameter: string = ""; previewAccessToken: string = ""; + previewRefreshToken: string = ""; + refreshQueryParameter: string = ""; title: string = "Welcome!"; buttonText: string = "Login"; buttonColor: Color = Color.primary();