diff --git a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx index 7a19a898f9..7ae2be788e 100644 --- a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx +++ b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx @@ -2,6 +2,7 @@ import { useMonaco } from '@monaco-editor/react' import { useTheme } from 'next-themes' import { PropsWithChildren, useMemo } from 'react' +import { ClockSkewBanner } from 'components/layouts/AppLayout/ClockSkewBanner' import IncidentBanner from 'components/layouts/AppLayout/IncidentBanner' import { NoticeBanner } from 'components/layouts/AppLayout/NoticeBanner' import { RestrictionBanner } from 'components/layouts/AppLayout/RestrictionBanner' @@ -16,6 +17,7 @@ const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => { const ongoingIncident = useFlag('ongoingIncident') const showNoticeBanner = useFlag('showNoticeBanner') + const clockSkewBanner = useFlag('clockSkewBanner') // Define the supabase theme for Monaco before anything is rendered. Using useEffect would sometime load the theme // after the editor was loaded, so it looked off. useMemo will always be run before rendering @@ -32,6 +34,7 @@ const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => { {ongoingIncident && } {showNoticeBanner && } {profile !== undefined && } + {clockSkewBanner && } {children} diff --git a/apps/studio/components/layouts/AppLayout/ClockSkewBanner.tsx b/apps/studio/components/layouts/AppLayout/ClockSkewBanner.tsx new file mode 100644 index 0000000000..2cc9cc500a --- /dev/null +++ b/apps/studio/components/layouts/AppLayout/ClockSkewBanner.tsx @@ -0,0 +1,58 @@ +import { BASE_PATH } from 'lib/constants' +import { useCallback, useEffect, useState } from 'react' +import { Button } from 'ui' + +// Show the banner if the clock skew is greater than 2 minutes +const CLOCK_SKEW_THRESHOLD = 2 * 60 * 1000 +// check every 5 minutes +const CLOCK_SKEW_CHECK_INTERVAL = 30 * 60 * 1000 + +const isClockSkewed = async () => { + try { + const response = await fetch(`${BASE_PATH}/api/get-utc-time`) + const data = await response.json() + // The received time is in UTC timezone, add Z at the end to make JS understand that + const serverTime = new Date(data.utcTime).getTime() + const clientTime = new Date().getTime() + const clockSkew = Math.abs(clientTime - serverTime) + + return clockSkew > CLOCK_SKEW_THRESHOLD + } catch { + return false + } +} + +export const ClockSkewBanner = () => { + const [clockSkew, setClockSkew] = useState(false) + + const checkClockSkew = useCallback(async () => { + const value = await isClockSkewed() + setClockSkew(value) + }, []) + + useEffect(() => { + // check for clock skew every CLOCK_SKEW_CHECK_INTERVAL + checkClockSkew() + const interval = setInterval(checkClockSkew, CLOCK_SKEW_CHECK_INTERVAL) + + return () => clearInterval(interval) + }, [checkClockSkew]) + + if (!clockSkew) return null + + return ( +
+

+ Your computer's clock appears to be inaccurate. This can cause issues with certain features. +

+ +
+ ) +} diff --git a/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx b/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx index c449ea96d2..78c18f5148 100644 --- a/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx +++ b/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useSelectedProject } from 'hooks/misc/useSelectedProject' -import { AlertTitle_Shadcn_, Alert_Shadcn_, Button, CriticalIcon, WarningIcon } from 'ui' +import { AlertTitle_Shadcn_, Alert_Shadcn_, Button, CriticalIcon, WarningIcon, cn } from 'ui' /** * Shown on projects in organization which are above their qouta @@ -18,7 +18,10 @@ export const RestrictionBanner = () => { return ( {currentOrg.restriction_status === 'restricted' ? : } diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts index b3ae6f5214..11f161156b 100644 --- a/apps/studio/middleware.ts +++ b/apps/studio/middleware.ts @@ -17,10 +17,10 @@ const HOSTED_SUPPORTED_API_URLS = [ '/ai/sql/cron', '/ai/docs', '/get-ip-address', + '/get-utc-time', ] export function middleware(request: NextRequest) { - const url = request.url if (IS_PLATFORM && !HOSTED_SUPPORTED_API_URLS.some((url) => request.url.endsWith(url))) { return Response.json( { success: false, message: 'Endpoint not supported on hosted' }, diff --git a/apps/studio/pages/api/get-utc-time.ts b/apps/studio/pages/api/get-utc-time.ts new file mode 100644 index 0000000000..0945a10c1f --- /dev/null +++ b/apps/studio/pages/api/get-utc-time.ts @@ -0,0 +1,11 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +// Returns the current UTC time in ISO format. Used to check if the client and server time are skewed. Clock skew causes +// issues with JWT verification. +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const utcTime = new Date().toISOString() + + return res.status(200).json({ utcTime }) +} + +export default handler