From 1e3dea91aa04f9b95adb9085ff941f2e795f7834 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 25 Apr 2023 17:01:26 +0800 Subject: [PATCH 01/70] Init API authorization page and scaffold RQ for API auth requests --- .../layouts/APIAuthorizationLayout.tsx | 49 ++++++++++ .../api-authorization-approve-mutation.ts | 43 +++++++++ .../api-authorization-decline-mutation.ts | 40 ++++++++ .../api-authorization-query.ts | 52 ++++++++++ studio/data/api-authorization/keys.ts | 3 + studio/data/organizations/keys.ts | 1 + .../data/organizations/organizations-query.ts | 37 +++++++ studio/pages/authorize.tsx | 96 +++++++++++++++++++ 8 files changed, 321 insertions(+) create mode 100644 studio/components/layouts/APIAuthorizationLayout.tsx create mode 100644 studio/data/api-authorization/api-authorization-approve-mutation.ts create mode 100644 studio/data/api-authorization/api-authorization-decline-mutation.ts create mode 100644 studio/data/api-authorization/api-authorization-query.ts create mode 100644 studio/data/api-authorization/keys.ts create mode 100644 studio/data/organizations/organizations-query.ts create mode 100644 studio/pages/authorize.tsx diff --git a/studio/components/layouts/APIAuthorizationLayout.tsx b/studio/components/layouts/APIAuthorizationLayout.tsx new file mode 100644 index 00000000000..f43656a4303 --- /dev/null +++ b/studio/components/layouts/APIAuthorizationLayout.tsx @@ -0,0 +1,49 @@ +import Head from 'next/head' +import Image from 'next/image' +import { useTheme } from 'common' +import { PropsWithChildren } from 'react' +import { BASE_PATH } from 'lib/constants' +import Divider from 'components/ui/Divider' + +export interface APIAuthorizationLayoutProps {} + +const APIAuthorizationLayout = ({ children }: PropsWithChildren) => { + const { isDarkMode } = useTheme() + return ( + <> + + Authorize API access | Supabase + +
+
+
+
+
+
+ Supabase + Supabase Logo +
+
+
+
+
+ +
+

Authorize API Access

+ {children} +
+
+ + ) +} + +export default APIAuthorizationLayout diff --git a/studio/data/api-authorization/api-authorization-approve-mutation.ts b/studio/data/api-authorization/api-authorization-approve-mutation.ts new file mode 100644 index 00000000000..5654041d540 --- /dev/null +++ b/studio/data/api-authorization/api-authorization-approve-mutation.ts @@ -0,0 +1,43 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +// import { resourceKeys } from './keys' + +export type ApiAuthorizationApproveVariables = { + id: string +} + +export async function approveApiAuthorization({ id }: ApiAuthorizationApproveVariables) { + if (!id) throw new Error('Authorization ID is required') + + const response = await post( + `${API_ADMIN_URL}/oauth/authorization/${id}?skip_browser_redirect=true`, + {} + ) + if (response.error) throw response.error + return response +} + +type ApiAuthorizationApproveData = Awaited> + +export const useApiAuthorizationApproveMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => approveApiAuthorization(vars), + { + async onSuccess(data, variables, context) { + // const { id } = variables + // await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef)) + // await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/api-authorization/api-authorization-decline-mutation.ts b/studio/data/api-authorization/api-authorization-decline-mutation.ts new file mode 100644 index 00000000000..f623f7d2905 --- /dev/null +++ b/studio/data/api-authorization/api-authorization-decline-mutation.ts @@ -0,0 +1,40 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { delete_ } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +// import { resourceKeys } from './keys' + +export type ApiAuthorizationDeclineVariables = { + id: string +} + +export async function declineApiAuthorization({ id }: ApiAuthorizationDeclineVariables) { + if (!id) throw new Error('Authorization ID is required') + + const response = await delete_(`${API_ADMIN_URL}/oauth/authorization/${id}`, {}) + if (response.error) throw response.error + return response +} + +type ApiAuthorizationDeclineData = Awaited> + +export const useApiAuthorizationDeclineMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => declineApiAuthorization(vars), + { + async onSuccess(data, variables, context) { + // const { id } = variables + // await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef)) + // await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/api-authorization/api-authorization-query.ts b/studio/data/api-authorization/api-authorization-query.ts new file mode 100644 index 00000000000..f687e49c403 --- /dev/null +++ b/studio/data/api-authorization/api-authorization-query.ts @@ -0,0 +1,52 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { useCallback } from 'react' +import { resourceKeys } from './keys' + +export type ApiAuthorizationVariables = { + id?: string +} + +export type ApiAuthorizationResponse = { + id: string +} + +export async function getApiAuthorizationDetails( + { id }: ApiAuthorizationVariables, + signal?: AbortSignal +) { + if (!id) throw new Error('Authorization ID is required') + + const response = await get(`${API_ADMIN_URL}/oauth/authorization/${id}`, { signal }) + if (response.error) throw response.error + return response as ApiAuthorizationResponse +} + +export type ResourceData = Awaited> +export type ResourceError = unknown + +export const useApiAuthorizationQuery = ( + { id }: ApiAuthorizationVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + resourceKeys.resource(id), + ({ signal }) => getApiAuthorizationDetails({ id }, signal), + { + enabled: enabled && typeof id !== 'undefined', + ...options, + } + ) + +export const useApiAuthorizationPrefetch = ({ id }: ApiAuthorizationVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (id) { + client.prefetchQuery(resourceKeys.resource(id), ({ signal }) => + getApiAuthorizationDetails({ id }, signal) + ) + } + }, [id]) +} diff --git a/studio/data/api-authorization/keys.ts b/studio/data/api-authorization/keys.ts new file mode 100644 index 00000000000..69660c24f42 --- /dev/null +++ b/studio/data/api-authorization/keys.ts @@ -0,0 +1,3 @@ +export const resourceKeys = { + resource: (id: string | undefined) => ['api-authorization', id] as const, +} diff --git a/studio/data/organizations/keys.ts b/studio/data/organizations/keys.ts index 5b7250c028d..4aabe753ab3 100644 --- a/studio/data/organizations/keys.ts +++ b/studio/data/organizations/keys.ts @@ -1,4 +1,5 @@ export const organizationKeys = { + list: () => ['organizations'] as const, detail: (slug: string | undefined) => ['organizations', slug, 'detail'] as const, roles: (slug: string | undefined) => ['organizations', slug, 'roles'] as const, freeProjectLimitCheck: (slug: string | undefined) => diff --git a/studio/data/organizations/organizations-query.ts b/studio/data/organizations/organizations-query.ts new file mode 100644 index 00000000000..598c8b17df4 --- /dev/null +++ b/studio/data/organizations/organizations-query.ts @@ -0,0 +1,37 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { useCallback } from 'react' +import { Organization } from 'types' +import { organizationKeys } from './keys' + +export type OrganizationsResponse = Organization[] + +export async function getOrganizations(signal?: AbortSignal) { + const data = await get(`${API_URL}/organizations`, { signal }) + if (data.error) throw data.error + + return data as OrganizationsResponse +} + +export type OrganizationsData = Awaited> +export type OrganizationsError = unknown + +export const useOrganizationsQuery = ({ + enabled = true, + ...options +}: UseQueryOptions = {}) => + useQuery( + organizationKeys.list(), + ({ signal }) => getOrganizations(signal), + { enabled: enabled, ...options } + ) + +export const useOrganizationsPrefetch = () => { + const client = useQueryClient() + + return useCallback( + () => client.prefetchQuery(organizationKeys.list(), ({ signal }) => getOrganizations(signal)), + [] + ) +} diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx new file mode 100644 index 00000000000..9d4e3aed96f --- /dev/null +++ b/studio/pages/authorize.tsx @@ -0,0 +1,96 @@ +import { useParams } from 'common' + +import { withAuth } from 'hooks' +import { NextPageWithLayout } from 'types' +import { FormPanel } from 'components/ui/Forms' +import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout' +import { Alert, Button, Listbox } from 'ui' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorization-query' + +const APIAuthorizationPage: NextPageWithLayout = () => { + const { auth_id } = useParams() + const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() + // const { data: apiAuthDetails, isLoading, isSuccess, isError } = useApiAuthorizationQuery({ id: auth_id }) + + // [Joshen] To be replaced with actual API call above + const apiAuthDetails = { + icon: 'https://cdn-icons-png.flaticon.com/512/5969/5969044.png', + host: 'cloudflare.com', + name: 'Cloudflare', + } + + const isExpired = false + + return ( + +
+ + +
+ + } + > +
+ {/* API Authorization requester details */} +
+

Authorize {apiAuthDetails.name}

+
+
+
+
+
+
+

+ {apiAuthDetails.name} is requesting API access to an organization. The application + will be able to{' '} + + read and write the organization's settings and projects + +

+
+
+ + {/* Expiry warning */} + {isExpired && ( + + Please retry your authorization request from the requesting app + + )} + + {/* Organization selection */} + {isLoadingOrganizations ? ( +
+ + +
+ ) : ( + + {(organizations ?? []).map((organization) => ( + + {organization.name} + + ))} + + )} +
+
+ ) +} + +APIAuthorizationPage.getLayout = (page) => {page} +export default withAuth(APIAuthorizationPage) From f65eb032c452f7b007ad0d6c6d5d98314e74d118 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 25 Apr 2023 17:06:58 +0800 Subject: [PATCH 02/70] Small UI change --- .../layouts/APIAuthorizationLayout.tsx | 1 - studio/pages/authorize.tsx | 33 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/studio/components/layouts/APIAuthorizationLayout.tsx b/studio/components/layouts/APIAuthorizationLayout.tsx index f43656a4303..143dd4a6526 100644 --- a/studio/components/layouts/APIAuthorizationLayout.tsx +++ b/studio/components/layouts/APIAuthorizationLayout.tsx @@ -38,7 +38,6 @@ const APIAuthorizationLayout = ({ children }: PropsWithChildren
-

Authorize API Access

{children}
diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index 9d4e3aed96f..c41f134f31f 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -25,6 +25,7 @@ const APIAuthorizationPage: NextPageWithLayout = () => { return ( Authorize API access for {apiAuthDetails.name}

} footer={
@@ -40,25 +41,23 @@ const APIAuthorizationPage: NextPageWithLayout = () => { >
{/* API Authorization requester details */} -
-

Authorize {apiAuthDetails.name}

-
-
-
-
-
+ +
+
+
+
-

- {apiAuthDetails.name} is requesting API access to an organization. The application - will be able to{' '} - - read and write the organization's settings and projects - -

+

+ {apiAuthDetails.name} is requesting API access to an organization. The application will + be able to{' '} + + read and write the organization's settings and projects + +

{/* Expiry warning */} From 63267c1fa9fe41d5c30aeec2ccccc9b624d9cdbc Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 25 Apr 2023 17:26:41 +0800 Subject: [PATCH 03/70] Scaffold error states to be handled --- studio/pages/authorize.tsx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index c41f134f31f..6161eca20ec 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -21,8 +21,38 @@ const APIAuthorizationPage: NextPageWithLayout = () => { name: 'Cloudflare', } + // [Joshen] To be replaced with actual API call above + const isApiAuthDetailsError = false const isExpired = false + if (auth_id === undefined) { + return ( + Authorization for API access

}> +
+ + Please provide a valid authorization ID in the URL + +
+
+ ) + } + + if (isApiAuthDetailsError) { + return ( + Authorization for API access

}> +
+ + Please retry your authorization request from the requesting app + +
+
+ ) + } + return ( Authorize API access for {apiAuthDetails.name}

} From f13c7299238e8a3102ebf49d45a41e7f93e1bf72 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Wed, 3 May 2023 15:42:56 +0200 Subject: [PATCH 04/70] chore: add analytics event to hp herop --- apps/www/components/Hero.tsx | 23 +++++++++++-- apps/www/hooks/useGoogleAnalyticsProps.tsx | 13 ++++++++ apps/www/lib/constants.ts | 2 ++ apps/www/lib/fetchWrapper.ts | 4 +-- apps/www/lib/gaEvents.ts | 20 ++++++++++++ apps/www/lib/telemetry.ts | 38 ++++++++++++++++++++++ apps/www/pages/_app.tsx | 2 +- 7 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 apps/www/hooks/useGoogleAnalyticsProps.tsx create mode 100644 apps/www/lib/gaEvents.ts create mode 100644 apps/www/lib/telemetry.ts diff --git a/apps/www/components/Hero.tsx b/apps/www/components/Hero.tsx index e97cd31fa74..5557da55251 100644 --- a/apps/www/components/Hero.tsx +++ b/apps/www/components/Hero.tsx @@ -1,10 +1,18 @@ -import { Button, IconBookOpen, Space } from 'ui' +import { Button, IconBookOpen } from 'ui' import Link from 'next/link' import { useRouter } from 'next/router' import SectionContainer from './Layouts/SectionContainer' +import Telemetry, { GoogleAnalyticsEvent } from '~/lib/telemetry' +import gaEvents from '~/lib/gaEvents' +import useGoogleAnalyticsProps from '~/hooks/useGoogleAnalyticsProps' const Hero = () => { const { basePath } = useRouter() + const googleAnalyticsProps = useGoogleAnalyticsProps() + + const handleClick = (event: GoogleAnalyticsEvent) => { + Telemetry.sendEvent(event, googleAnalyticsProps) + } return (
@@ -41,14 +49,23 @@ const Hero = () => {
- - diff --git a/apps/www/hooks/useGoogleAnalyticsProps.tsx b/apps/www/hooks/useGoogleAnalyticsProps.tsx new file mode 100644 index 00000000000..a3254a8b307 --- /dev/null +++ b/apps/www/hooks/useGoogleAnalyticsProps.tsx @@ -0,0 +1,13 @@ +import { useRouter } from 'next/router' + +const useGoogleAnalyticsProps = () => { + const { locale } = useRouter() + + return { + screenResolution: + typeof window !== 'undefined' ? `${window.innerWidth}x${window.innerHeight}` : undefined, + language: locale ?? 'en-US', + } +} + +export default useGoogleAnalyticsProps diff --git a/apps/www/lib/constants.ts b/apps/www/lib/constants.ts index e3c815b5dca..1b7537a438d 100644 --- a/apps/www/lib/constants.ts +++ b/apps/www/lib/constants.ts @@ -1,5 +1,7 @@ export const APP_NAME = 'Supabase' export const DESCRIPTION = 'The Open Source Alternative to Firebase.' +export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' +export const API_URL = !IS_PROD ? process.env.NEXT_PUBLIC_API_URL : '/api' // Launch Weeek export const SAMPLE_TICKET_NUMBER = 1234 diff --git a/apps/www/lib/fetchWrapper.ts b/apps/www/lib/fetchWrapper.ts index c573a0d37dc..0313e49565f 100644 --- a/apps/www/lib/fetchWrapper.ts +++ b/apps/www/lib/fetchWrapper.ts @@ -1,7 +1,5 @@ interface DataProps { - referrer?: string - title: string - route?: string + [prop: string]: any } export const post = (url: string, data: DataProps, options = {}) => { diff --git a/apps/www/lib/gaEvents.ts b/apps/www/lib/gaEvents.ts new file mode 100644 index 00000000000..53908751474 --- /dev/null +++ b/apps/www/lib/gaEvents.ts @@ -0,0 +1,20 @@ +import { GoogleAnalyticsEvent } from './telemetry' + +interface GoogleAnalyticsEventType { + [key: string]: GoogleAnalyticsEvent +} + +const GoogleAnalyticsEvents: GoogleAnalyticsEventType = { + www_hp_hero_startProject: { + action: 'www_hp_hero_startProject', + category: 'link', + label: 'Clicks on “Start your project” button in the home page hero', + }, + www_hp_hero_documentation: { + action: 'www_hp_hero_documentation', + category: 'link', + label: 'Clicks on “Documentation” button in the home page hero', + }, +} + +export default GoogleAnalyticsEvents diff --git a/apps/www/lib/telemetry.ts b/apps/www/lib/telemetry.ts new file mode 100644 index 00000000000..cdaa08cf1e8 --- /dev/null +++ b/apps/www/lib/telemetry.ts @@ -0,0 +1,38 @@ +import { post } from '~/lib/fetchWrapper' +import { API_URL, IS_PROD } from 'lib/constants' + +export interface GoogleAnalyticsEvent { + category: string + action: string + label: string + value?: string +} + +export interface GoogleAnalyticsProps { + screenResolution?: string + language: string +} + +// This event is the same as in studio/lib/telemetry.tx +// but uses different ENV variables for www + +const sendEvent = (event: GoogleAnalyticsEvent, gaProps: GoogleAnalyticsProps) => { + if (!IS_PROD) return + + const { category, action, label, value } = event + + return post(`${API_URL}/telemetry/event`, { + action: action, + category: category, + label: label, + value: value, + ga: { + screen_resolution: gaProps?.screenResolution, + language: gaProps?.language, + }, + }) +} + +export default { + sendEvent, +} diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index 2ccb053b3ae..60a1a4684fe 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' import { useEffect } from 'react' import Meta from '~/components/Favicons' import '../styles/index.css' -import { post } from './../lib/fetchWrapper' +import { post } from '~/lib/fetchWrapper' import { AuthProvider, ThemeProvider } from 'common' import Head from 'next/head' From da33e3bea6a2507da6f995d5ecdfd47ba678f72f Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Wed, 3 May 2023 16:09:32 +0200 Subject: [PATCH 05/70] chore: navigate with router --- apps/www/components/Hero.tsx | 58 ++++++++++++++++++------------------ apps/www/lib/constants.ts | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/www/components/Hero.tsx b/apps/www/components/Hero.tsx index 5557da55251..53df050350e 100644 --- a/apps/www/components/Hero.tsx +++ b/apps/www/components/Hero.tsx @@ -7,11 +7,12 @@ import gaEvents from '~/lib/gaEvents' import useGoogleAnalyticsProps from '~/hooks/useGoogleAnalyticsProps' const Hero = () => { - const { basePath } = useRouter() + const router = useRouter() const googleAnalyticsProps = useGoogleAnalyticsProps() - const handleClick = (event: GoogleAnalyticsEvent) => { + const trackAndNavigate = (url: string, event: GoogleAnalyticsEvent) => { Telemetry.sendEvent(event, googleAnalyticsProps) + router.push(url) } return ( @@ -47,29 +48,28 @@ const Hero = () => {
- - - - - - - - - - + +
backed by @@ -77,22 +77,22 @@ const Hero = () => {
Y Combinator Mozilla Coatue Felicis
diff --git a/apps/www/lib/constants.ts b/apps/www/lib/constants.ts index 1b7537a438d..5586bb59f81 100644 --- a/apps/www/lib/constants.ts +++ b/apps/www/lib/constants.ts @@ -1,7 +1,7 @@ export const APP_NAME = 'Supabase' export const DESCRIPTION = 'The Open Source Alternative to Firebase.' export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' -export const API_URL = !IS_PROD ? process.env.NEXT_PUBLIC_API_URL : '/api' +export const API_URL = IS_PROD ? process.env.NEXT_PUBLIC_API_URL : '/api' // Launch Weeek export const SAMPLE_TICKET_NUMBER = 1234 From 9952fec9d9ebd9e5edea58f00851670b249b510d Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Wed, 3 May 2023 22:58:47 +0000 Subject: [PATCH 06/70] Add password reset guide to the auth section --- .../NavigationMenu.constants.ts | 1 + .../pages/guides/auth/auth-password-reset.mdx | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 apps/docs/pages/guides/auth/auth-password-reset.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 78292feff7e..d2d9a91bfe6 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -295,6 +295,7 @@ export const auth = { }, ], }, + { name: 'Password Reset', url: '/guides/auth/auth-password-reset' }, { name: 'Email Templates', url: '/guides/auth/auth-email-templates' }, ], }, diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx new file mode 100644 index 00000000000..88664550455 --- /dev/null +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -0,0 +1,104 @@ +import Layout from '~/layouts/DefaultGuideLayout' +import { Accordion } from 'ui' + +export const meta = { + id: 'auth-password-reset', + title: 'Password Reset', + description: 'How to reset your password for your Supabase project', +} + +- Send email for password reset using [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail) while providing a `redirectTo` parameter +- Email link will work as a magic link and log the user in then take them to the url specified in the `redirectTo` parameter +- Create form to update the password and call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method with the new password + +## Single Page Application (SPA) + +### Sending password reset email +Supabase provides a convenient method [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail) +to reset a user password. This method takes a parameter of `redirectTo` which we will use to pass an absolute URL to +the update password page. This url must be saved in your allowed [Redirect URLs](https://app.supabase.com/project/sgbluorgiylcyqpqihab/auth/url-configuration) +list or it won't work. + +```ts +await supabase.auth.resetPasswordForEmail("hello@example.com", { + redirectTo: 'http://example.com/account/update-password' +}) +``` + +### Email link +The email link you receive will work like a magic link. This way when you click the link you will be logged into +the website. Since we passed a redirect URL to the [`.resetPasswordForEmail`](https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail) +method the user should be sent to the update password page. + +### Update Password +To update the password we call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method and pass along the new password to this method. + +```ts +await supabase.auth.updateUser({ password: new_password }) +``` + +## Server-Side Rendering (SSR) + +### Sending password reset email +Supabase provides a convenient method [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail) +to reset a user password. This method takes a parameter of `redirectTo` which we will use to pass an absolute URL to +the callback page along with a query parameter to the update password page. This URL must be saved in your +allowed [Redirect URLs](https://app.supabase.com/project/_/auth/url-configuration) list found at +[Authentication > Redirect Configuration](https://app.supabase.com/project/_/auth/url-configuration) or it won't +redirect the user. + +```ts +await supabase.auth.resetPasswordForEmail("hello@example.com", { + redirectTo: 'http://example.com/auth/callback?next=/account/update-password' +}) +``` + + + +We are using `next` as our query parameter but this is just personal preference, you could +use a different query parameter name. + + + +### Email link +The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the code exchange code. + +### Exchange authorization code +After getting redirected to the server page we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function. +```ts +// code can be retrieved using your framework of choice way of getting query parameter from the url. +// This is just example code +const code = url.searchParams.get('code') + +// call the Supabase API to exchange the code for a session +await supabase.auth.exchangeCodeForSession(code) +``` + + + +The query parameter is always `code` for the authorization code returned from the Supabase API + + + +We will also need to check for the `next` query parameter to redirect the user to the update password page. +```ts +// password page path can be retrieved using your framework of choice way of getting query parameter from the url. +// This is just example code +const next = url.searchParams.get('next') + +// using NextJS response object in this example +res.redirect(next) +``` + +### Update Password +To update the password we call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method and +pass along the new password to this method. + +```ts +await supabase.auth.updateUser({ password: new_password }) +``` + +export const Page = ({ children }) => + +export default Page + From 3ab93886d9d6d17c5c2cd27ae5f0fd1fa70c5dd2 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Wed, 3 May 2023 23:23:46 +0000 Subject: [PATCH 07/70] Update code snippets with filename --- apps/docs/pages/guides/auth/auth-password-reset.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx index 88664550455..9bd052a0345 100644 --- a/apps/docs/pages/guides/auth/auth-password-reset.mdx +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -49,7 +49,7 @@ redirect the user. ```ts await supabase.auth.resetPasswordForEmail("hello@example.com", { - redirectTo: 'http://example.com/auth/callback?next=/account/update-password' + redirectTo: 'http://example.com/api/auth/callback?next=/account/update-password' }) ``` @@ -66,6 +66,8 @@ The email link you receive will behave like a magic link. When the link is click ### Exchange authorization code After getting redirected to the server page we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function. ```ts +// api/auth/callback.ts + // code can be retrieved using your framework of choice way of getting query parameter from the url. // This is just example code const code = url.searchParams.get('code') @@ -82,6 +84,8 @@ The query parameter is always `code` for the authorization code returned from th We will also need to check for the `next` query parameter to redirect the user to the update password page. ```ts +// api/auth/callback.ts + // password page path can be retrieved using your framework of choice way of getting query parameter from the url. // This is just example code const next = url.searchParams.get('next') From 5e2dca6cc07904482590eefb7e07e2ddcd3c8a9a Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Wed, 3 May 2023 16:19:51 +0200 Subject: [PATCH 08/70] chore: update API_URL preview check --- apps/www/components/Hero.tsx | 5 ++--- apps/www/lib/constants.ts | 3 ++- apps/www/lib/gaEvents.ts | 4 ++-- apps/www/lib/telemetry.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/www/components/Hero.tsx b/apps/www/components/Hero.tsx index 53df050350e..32f699d2939 100644 --- a/apps/www/components/Hero.tsx +++ b/apps/www/components/Hero.tsx @@ -1,5 +1,4 @@ import { Button, IconBookOpen } from 'ui' -import Link from 'next/link' import { useRouter } from 'next/router' import SectionContainer from './Layouts/SectionContainer' import Telemetry, { GoogleAnalyticsEvent } from '~/lib/telemetry' @@ -10,8 +9,8 @@ const Hero = () => { const router = useRouter() const googleAnalyticsProps = useGoogleAnalyticsProps() - const trackAndNavigate = (url: string, event: GoogleAnalyticsEvent) => { - Telemetry.sendEvent(event, googleAnalyticsProps) + const trackAndNavigate = async (url: string, event: GoogleAnalyticsEvent) => { + await Telemetry.sendEvent(event, googleAnalyticsProps) router.push(url) } diff --git a/apps/www/lib/constants.ts b/apps/www/lib/constants.ts index 5586bb59f81..445c0a6810a 100644 --- a/apps/www/lib/constants.ts +++ b/apps/www/lib/constants.ts @@ -1,7 +1,8 @@ export const APP_NAME = 'Supabase' export const DESCRIPTION = 'The Open Source Alternative to Firebase.' export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' -export const API_URL = IS_PROD ? process.env.NEXT_PUBLIC_API_URL : '/api' +export const IS_PREVIEW = process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' +export const API_URL = process.env.NEXT_PUBLIC_API_URL // Launch Weeek export const SAMPLE_TICKET_NUMBER = 1234 diff --git a/apps/www/lib/gaEvents.ts b/apps/www/lib/gaEvents.ts index 53908751474..167aa356a0c 100644 --- a/apps/www/lib/gaEvents.ts +++ b/apps/www/lib/gaEvents.ts @@ -8,12 +8,12 @@ const GoogleAnalyticsEvents: GoogleAnalyticsEventType = { www_hp_hero_startProject: { action: 'www_hp_hero_startProject', category: 'link', - label: 'Clicks on “Start your project” button in the home page hero', + label: '', }, www_hp_hero_documentation: { action: 'www_hp_hero_documentation', category: 'link', - label: 'Clicks on “Documentation” button in the home page hero', + label: '', }, } diff --git a/apps/www/lib/telemetry.ts b/apps/www/lib/telemetry.ts index cdaa08cf1e8..0c6fb2ed9fe 100644 --- a/apps/www/lib/telemetry.ts +++ b/apps/www/lib/telemetry.ts @@ -1,5 +1,5 @@ import { post } from '~/lib/fetchWrapper' -import { API_URL, IS_PROD } from 'lib/constants' +import { API_URL, IS_PROD, IS_PREVIEW } from 'lib/constants' export interface GoogleAnalyticsEvent { category: string @@ -17,7 +17,7 @@ export interface GoogleAnalyticsProps { // but uses different ENV variables for www const sendEvent = (event: GoogleAnalyticsEvent, gaProps: GoogleAnalyticsProps) => { - if (!IS_PROD) return + if (!IS_PROD && !IS_PREVIEW) return const { category, action, label, value } = event From 6c33e30f4c91fb8a41ea123d25905314aacddacd Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 4 May 2023 12:42:11 +0200 Subject: [PATCH 09/70] chore: update telemetry API_URL on page load From 77ecbde3c7873f9f3b4732a54c564bfdda222298 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 4 May 2023 12:59:16 +0200 Subject: [PATCH 10/70] chore: update telemetry API_URL on page load --- apps/www/pages/_app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index 60a1a4684fe..dce4295802c 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -1,4 +1,4 @@ -import { APP_NAME, DESCRIPTION } from 'lib/constants' +import { API_URL, APP_NAME, DESCRIPTION } from 'lib/constants' import { DefaultSeo } from 'next-seo' import { AppProps } from 'next/app' import { useRouter } from 'next/router' @@ -13,7 +13,7 @@ export default function MyApp({ Component, pageProps }: AppProps) { const router = useRouter() function handlePageTelemetry(route: string) { - return post(`https://api.supabase.io/platform/telemetry/page`, { + return post(`${API_URL}/telemetry/page`, { referrer: document.referrer, title: document.title, route, From eaddf931eb820248938555f4bbd5ec43aea2e642 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 4 May 2023 13:49:16 +0200 Subject: [PATCH 11/70] chore: update hardcoded API_URL in docs page telemetry --- apps/docs/lib/constants.ts | 1 + apps/docs/pages/_app.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/docs/lib/constants.ts b/apps/docs/lib/constants.ts index d1bbedbf47c..42c12c0d3f4 100644 --- a/apps/docs/lib/constants.ts +++ b/apps/docs/lib/constants.ts @@ -1,2 +1,3 @@ export const IS_PLATFORM = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' export const LOCAL_SUPABASE = process.env.NEXT_PUBLIC_LOCAL_SUPABASE === 'true' +export const API_URL = process.env.NEXT_PUBLIC_API_URL diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index 0ac7df56c63..ed75637efaa 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -10,7 +10,7 @@ import { CommandMenuProvider } from 'ui' import components from '~/components' import Favicons from '~/components/Favicons' import SiteLayout from '~/layouts/SiteLayout' -import { IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants' +import { API_URL, IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants' import { post } from '~/lib/fetchWrappers' import '../styles/ch.scss' import '../styles/main.scss?v=1.0.0' @@ -25,7 +25,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ) function handlePageTelemetry(route: string) { - return post(`https://api.supabase.io/platform/telemetry/page`, { + return post(`${API_URL}/telemetry/page`, { referrer: document.referrer, title: document.title, route, From 03f7a509285e05365bbafead2eb47df9c70627ee Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 4 May 2023 17:12:32 +0200 Subject: [PATCH 12/70] chore: add product links ga events --- apps/www/components/Features/index.tsx | 48 ++++++++++++++++++++++-- apps/www/components/Hero.tsx | 52 ++++++++++++-------------- apps/www/components/TextLink.tsx | 28 +++++++------- apps/www/lib/gaEvents.ts | 25 +++++++++++++ 4 files changed, 107 insertions(+), 46 deletions(-) diff --git a/apps/www/components/Features/index.tsx b/apps/www/components/Features/index.tsx index b5c8dc3f68f..132690287bf 100644 --- a/apps/www/components/Features/index.tsx +++ b/apps/www/components/Features/index.tsx @@ -1,12 +1,46 @@ -import { Button, Badge, IconArrowRight } from 'ui' -import SectionHeader from 'components/UI/SectionHeader' +import { Badge } from 'ui' import Solutions from 'data/Solutions.json' -import Link from 'next/link' +import useGoogleAnalyticsProps from '~/hooks/useGoogleAnalyticsProps' +import Telemetry from '~/lib/telemetry' +import gaEvents from '~/lib/gaEvents' import SectionContainer from '../Layouts/SectionContainer' import ProductIcon from '../ProductIcon' import TextLink from '../TextLink' const Features = () => { + const googleAnalyticsProps = useGoogleAnalyticsProps() + + const sendTelemetryEvent = async (product: any) => { + switch (product) { + case 'Database': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_database'], + googleAnalyticsProps + ) + case 'Authentication': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_auth'], + googleAnalyticsProps + ) + case 'Storage': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_storage'], + googleAnalyticsProps + ) + case 'Edge Functions': + console.log('edge!!') + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_edgeFunctions'], + googleAnalyticsProps + ) + case 'Realtime': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_realtime'], + googleAnalyticsProps + ) + } + } + const IconSections = Object.values(Solutions).map((solution: any) => { const { name, description, icon, label, url } = solution if (solution.name === 'Realtime') return null @@ -24,7 +58,13 @@ const Features = () => { {label}
)} - {url && } + {url && ( + sendTelemetryEvent(name)} + /> + )}
) }) diff --git a/apps/www/components/Hero.tsx b/apps/www/components/Hero.tsx index 32f699d2939..b532225359b 100644 --- a/apps/www/components/Hero.tsx +++ b/apps/www/components/Hero.tsx @@ -1,17 +1,17 @@ import { Button, IconBookOpen } from 'ui' import { useRouter } from 'next/router' +import Link from 'next/link' import SectionContainer from './Layouts/SectionContainer' import Telemetry, { GoogleAnalyticsEvent } from '~/lib/telemetry' import gaEvents from '~/lib/gaEvents' import useGoogleAnalyticsProps from '~/hooks/useGoogleAnalyticsProps' const Hero = () => { - const router = useRouter() + const { basePath } = useRouter() const googleAnalyticsProps = useGoogleAnalyticsProps() - const trackAndNavigate = async (url: string, event: GoogleAnalyticsEvent) => { + const sendTelemetryEvent = async (event: GoogleAnalyticsEvent) => { await Telemetry.sendEvent(event, googleAnalyticsProps) - router.push(url) } return ( @@ -47,28 +47,22 @@ const Hero = () => {
- - + + sendTelemetryEvent(gaEvents['www_hp_hero_startProject'])}> + + + + + sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])} + > + + +
backed by @@ -76,22 +70,22 @@ const Hero = () => {
Y Combinator Mozilla Coatue Felicis
diff --git a/apps/www/components/TextLink.tsx b/apps/www/components/TextLink.tsx index 3bd3edfc10e..5da959a41fe 100644 --- a/apps/www/components/TextLink.tsx +++ b/apps/www/components/TextLink.tsx @@ -1,20 +1,22 @@ -import { IconArrowRight, IconChevronRight } from 'ui' +import { IconChevronRight } from 'ui' import Link from 'next/link' -function TextLink({ url, label }: any) { +function TextLink({ url, label, ...props }: any) { return ( - -
- {`${label} about ${url}`} - {label} - - + + ) } diff --git a/apps/www/lib/gaEvents.ts b/apps/www/lib/gaEvents.ts index 167aa356a0c..73599720f86 100644 --- a/apps/www/lib/gaEvents.ts +++ b/apps/www/lib/gaEvents.ts @@ -15,6 +15,31 @@ const GoogleAnalyticsEvents: GoogleAnalyticsEventType = { category: 'link', label: '', }, + www_hp_subhero_products_database: { + action: 'www_hp_subhero_products_database', + category: 'link', + label: '', + }, + www_hp_subhero_products_auth: { + action: 'www_hp_subhero_products_auth', + category: 'link', + label: '', + }, + www_hp_subhero_products_storage: { + action: 'www_hp_subhero_products_storage', + category: 'link', + label: '', + }, + www_hp_subhero_products_edgeFunctions: { + action: 'www_hp_subhero_products_edgeFunctions', + category: 'link', + label: '', + }, + www_hp_subhero_products_realtime: { + action: 'www_hp_subhero_products_realtime', + category: 'link', + label: '', + }, } export default GoogleAnalyticsEvents From 4715044f5672be33ccbf53df4bd3e3d576ed1ab5 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 4 May 2023 17:22:46 +0200 Subject: [PATCH 13/70] chore: fix TextLink href --- apps/www/components/Features/index.tsx | 1 - apps/www/components/TextLink.tsx | 4 ++-- apps/www/data/Solutions.json | 8 -------- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/www/components/Features/index.tsx b/apps/www/components/Features/index.tsx index 132690287bf..bb84b4dbf4c 100644 --- a/apps/www/components/Features/index.tsx +++ b/apps/www/components/Features/index.tsx @@ -28,7 +28,6 @@ const Features = () => { googleAnalyticsProps ) case 'Edge Functions': - console.log('edge!!') return await Telemetry.sendEvent( gaEvents['www_hp_subhero_products_edgeFunctions'], googleAnalyticsProps diff --git a/apps/www/components/TextLink.tsx b/apps/www/components/TextLink.tsx index 5da959a41fe..73d54f6136e 100644 --- a/apps/www/components/TextLink.tsx +++ b/apps/www/components/TextLink.tsx @@ -1,9 +1,9 @@ import { IconChevronRight } from 'ui' import Link from 'next/link' -function TextLink({ url, label, ...props }: any) { +function TextLink({ url = '', label, ...props }: any) { return ( - + Date: Thu, 4 May 2023 17:24:50 +0200 Subject: [PATCH 14/70] chore: fix docs link --- apps/www/components/Hero.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/components/Hero.tsx b/apps/www/components/Hero.tsx index b532225359b..28cbc079bca 100644 --- a/apps/www/components/Hero.tsx +++ b/apps/www/components/Hero.tsx @@ -54,7 +54,7 @@ const Hero = () => { - + sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])} > From 29fe25026e7c10d8f6e5687de0ffa80ca2ca03a1 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Thu, 4 May 2023 23:06:42 -0230 Subject: [PATCH 15/70] Adjust menu item spacing and highlighting, scroll to active menu item on page load --- .../NavigationMenu/NavigationMenuCliList.tsx | 2 +- .../NavigationMenuGuideListItems.tsx | 41 ++++++--- .../NavigationMenuRefListItems.tsx | 85 ++++++++++++------- 3 files changed, 84 insertions(+), 44 deletions(-) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx index cae120747b8..9c15657d064 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx @@ -22,7 +22,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => { icon?: string }) => { return ( -
  • +
  • {icon && } diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index 22a7d049827..a5ddbec9512 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -2,7 +2,7 @@ import { useTheme } from 'common/Providers' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' -import React from 'react' +import React, { useEffect, useRef } from 'react' import { IconChevronLeft } from '~/../../packages/ui' import * as Accordion from '@radix-ui/react-accordion' @@ -43,6 +43,8 @@ const HeaderLink = React.memo(function HeaderLink(props: { const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any) { const router = useRouter() const { isDarkMode } = useTheme() + const activeItem = props.subItem.url === router.pathname + const activeItemRef = useRef(null) const LinkContainer = (props) => { return ( @@ -52,6 +54,15 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any ) } + useEffect(() => { + // scroll to active item + if (activeItem && activeItemRef.current) { + // this is a hack, but seems a common one on Stackoverflow + setTimeout(() => { + activeItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + }, 0) + } + }) return ( <> {props.subItemIndex === 0 && ( @@ -63,15 +74,15 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any )} -
  • +
  • @@ -120,14 +131,14 @@ const ContentLink = React.memo(function ContentLink(props: any) { const router = useRouter() return ( -
  • +
  • {props.icon && ( @@ -179,15 +190,17 @@ const Content = (props) => { return (
    {x.items && x.items.length > 0 ? ( -
    +
    {x.items.map((subItem, subItemIndex) => { return ( - + <> + + ) })}
    diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx index b81f6c5a1dd..dffa2c2d482 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx @@ -1,7 +1,7 @@ import * as Accordion from '@radix-ui/react-accordion' import Link from 'next/link' import { useRouter } from 'next/router' -import { IconChevronLeft } from 'ui' +import { IconChevronLeft, IconChevronUp } from 'ui' import * as NavItems from './NavigationMenu.constants' import { find } from 'lodash' @@ -15,6 +15,7 @@ import { RefIdOptions, RefKeyOptions } from './NavigationMenu' import React, { Fragment } from 'react' import { generateAllowedClientLibKeys } from '~/lib/refGenerator/helpers' import { isFuncNotInLibraryOrVersion } from './NavigationMenu.utils' +import { cn } from 'ui/src/utils/cn' const HeaderImage = React.memo(function HeaderImage(props: any) { const router = useRouter() @@ -31,8 +32,6 @@ const HeaderImage = React.memo(function HeaderImage(props: any) { }) const HeaderLink = React.memo(function HeaderLink(props: any) { - const router = useRouter() - return ( {props.title ?? props.id} @@ -46,6 +45,8 @@ const FunctionLink = React.memo(function FunctionLink({ icon, library, slug, + isParent = false, + isSubItem = false, }: { title: string name?: string @@ -54,6 +55,8 @@ const FunctionLink = React.memo(function FunctionLink({ product?: string library: string slug: string + isParent: boolean + isSubItem: boolean }) { const router = useRouter() const activeAccordianItem = useMenuActiveRefId() @@ -70,16 +73,29 @@ const FunctionLink = React.memo(function FunctionLink({ const active = activeAccordianItem === id return ( -
  • +
  • {icon && {icon}} {title} + {active && !isSubItem && ( + + )} + {isParent && ( + + )}
  • @@ -121,26 +137,37 @@ const RenderLink = React.memo(function RenderLink(props: any) { value={active ? props.id : ''} > - - - {props.items && - props.items - .filter((item) => props.allowedKeys.includes(item.id)) - .map((item) => { - return ( - - ) - })} - + + {props.items && ( + + {props.items && + props.items + .filter((item) => props.allowedKeys.includes(item.id)) + .map((item) => { + return ( + + ) + })} + + )} ) @@ -244,7 +271,7 @@ const Content: React.FC = ({ id, lib, commonSections, sp
    -
      +
        {sections.map((fn: any, fnIndex) => { // run allow check if (isFuncNotInLibraryOrVersion(fn.id, fn.type, allowedKeys)) { From ac10f883b6c483a7f406dcc8df5cd7bfc4460827 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 5 May 2023 17:44:31 +0800 Subject: [PATCH 16/70] Hook up endpoints --- .../api-authorization-approve-mutation.ts | 31 ++-- .../api-authorization-decline-mutation.ts | 22 +-- .../api-authorization-query.ts | 28 ++- studio/pages/authorize.tsx | 170 ++++++++++++++---- 4 files changed, 186 insertions(+), 65 deletions(-) diff --git a/studio/data/api-authorization/api-authorization-approve-mutation.ts b/studio/data/api-authorization/api-authorization-approve-mutation.ts index 5654041d540..5ac01111adc 100644 --- a/studio/data/api-authorization/api-authorization-approve-mutation.ts +++ b/studio/data/api-authorization/api-authorization-approve-mutation.ts @@ -1,21 +1,29 @@ -import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { post } from 'lib/common/fetch' import { API_ADMIN_URL } from 'lib/constants' -// import { resourceKeys } from './keys' export type ApiAuthorizationApproveVariables = { id: string + organization_id: string } -export async function approveApiAuthorization({ id }: ApiAuthorizationApproveVariables) { +export type ApiAuthorizationApproveResponse = { + url: string +} + +export async function approveApiAuthorization({ + id, + organization_id, +}: ApiAuthorizationApproveVariables) { if (!id) throw new Error('Authorization ID is required') + if (!organization_id) throw new Error('Organization slug is required') const response = await post( - `${API_ADMIN_URL}/oauth/authorization/${id}?skip_browser_redirect=true`, - {} + `${API_ADMIN_URL}/oauth/authorizations/${id}?skip_browser_redirect=true`, + { organization_id } ) if (response.error) throw response.error - return response + return response as ApiAuthorizationApproveResponse } type ApiAuthorizationApproveData = Awaited> @@ -27,17 +35,8 @@ export const useApiAuthorizationApproveMutation = ({ UseMutationOptions, 'mutationFn' > = {}) => { - const queryClient = useQueryClient() - return useMutation( (vars) => approveApiAuthorization(vars), - { - async onSuccess(data, variables, context) { - // const { id } = variables - // await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef)) - // await onSuccess?.(data, variables, context) - }, - ...options, - } + options ) } diff --git a/studio/data/api-authorization/api-authorization-decline-mutation.ts b/studio/data/api-authorization/api-authorization-decline-mutation.ts index f623f7d2905..7825bb01688 100644 --- a/studio/data/api-authorization/api-authorization-decline-mutation.ts +++ b/studio/data/api-authorization/api-authorization-decline-mutation.ts @@ -1,18 +1,21 @@ -import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { delete_ } from 'lib/common/fetch' import { API_ADMIN_URL } from 'lib/constants' -// import { resourceKeys } from './keys' export type ApiAuthorizationDeclineVariables = { id: string } +export type ApiAuthorizationDeclineResponse = { + id: string +} + export async function declineApiAuthorization({ id }: ApiAuthorizationDeclineVariables) { if (!id) throw new Error('Authorization ID is required') - const response = await delete_(`${API_ADMIN_URL}/oauth/authorization/${id}`, {}) + const response = await delete_(`${API_ADMIN_URL}/oauth/authorizations/${id}`, {}) if (response.error) throw response.error - return response + return response as ApiAuthorizationDeclineResponse } type ApiAuthorizationDeclineData = Awaited> @@ -24,17 +27,8 @@ export const useApiAuthorizationDeclineMutation = ({ UseMutationOptions, 'mutationFn' > = {}) => { - const queryClient = useQueryClient() - return useMutation( (vars) => declineApiAuthorization(vars), - { - async onSuccess(data, variables, context) { - // const { id } = variables - // await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef)) - // await onSuccess?.(data, variables, context) - }, - ...options, - } + options ) } diff --git a/studio/data/api-authorization/api-authorization-query.ts b/studio/data/api-authorization/api-authorization-query.ts index f687e49c403..bf73ad67872 100644 --- a/studio/data/api-authorization/api-authorization-query.ts +++ b/studio/data/api-authorization/api-authorization-query.ts @@ -9,7 +9,12 @@ export type ApiAuthorizationVariables = { } export type ApiAuthorizationResponse = { - id: string + name: string + website: string + domain: string + expires_at: string + approved_at: string | null + approved_organization_slug?: string } export async function getApiAuthorizationDetails( @@ -18,8 +23,25 @@ export async function getApiAuthorizationDetails( ) { if (!id) throw new Error('Authorization ID is required') - const response = await get(`${API_ADMIN_URL}/oauth/authorization/${id}`, { signal }) - if (response.error) throw response.error + const response = await get(`${API_ADMIN_URL}/oauth/authorizations/${id}`, { signal }) + if (response.error) { + // 404 is a valid error in which the auth id is invalid + const isInvalid = + (response.error as any)?.code === 404 && + (response.error as any)?.message?.includes('OAuth authorization request does not exist') + + if (isInvalid) { + return { + name: '', + website: '', + domain: '', + expires_at: '', + approved_at: null, + } as ApiAuthorizationResponse + } + + throw response.error + } return response as ApiAuthorizationResponse } diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index 6161eca20ec..5ea6e61ae0a 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -1,29 +1,101 @@ import { useParams } from 'common' - -import { withAuth } from 'hooks' -import { NextPageWithLayout } from 'types' -import { FormPanel } from 'components/ui/Forms' import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout' -import { Alert, Button, Listbox } from 'ui' -import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import { FormPanel } from 'components/ui/Forms' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useApiAuthorizationApproveMutation } from 'data/api-authorization/api-authorization-approve-mutation' +import { useApiAuthorizationDeclineMutation } from 'data/api-authorization/api-authorization-decline-mutation' import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorization-query' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import dayjs from 'dayjs' +import { useStore, withAuth } from 'hooks' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { NextPageWithLayout } from 'types' +import { Alert, Button, Listbox } from 'ui' + +// Need to handle if no organizations in account +// Need to handle if not logged in yet state const APIAuthorizationPage: NextPageWithLayout = () => { + const { ui } = useStore() + const router = useRouter() const { auth_id } = useParams() - const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() - // const { data: apiAuthDetails, isLoading, isSuccess, isError } = useApiAuthorizationQuery({ id: auth_id }) + const [isSubmitting, setIsSubmitting] = useState(false) + const [selectedOrg, setSelectedOrg] = useState() - // [Joshen] To be replaced with actual API call above - const apiAuthDetails = { - icon: 'https://cdn-icons-png.flaticon.com/512/5969/5969044.png', - host: 'cloudflare.com', - name: 'Cloudflare', + const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() + const { data: requester, isLoading, isError } = useApiAuthorizationQuery({ id: auth_id }) + const isApproved = requester?.approved_at !== null + const isInvalid = requester?.name.length === 0 && requester.expires_at.length === 0 + const isExpired = dayjs().isAfter(dayjs(requester?.expires_at)) + + const { mutateAsync: approveRequest } = useApiAuthorizationApproveMutation() + const { mutateAsync: declineRequest } = useApiAuthorizationDeclineMutation() + + useEffect(() => { + if (!isLoadingOrganizations) { + setSelectedOrg(organizations?.[0].slug ?? undefined) + } + }, [isLoadingOrganizations]) + + const onApproveRequest = async () => { + if (!auth_id) { + return ui.setNotification({ + category: 'error', + message: 'Unable to approve request: auth_id is missing ', + }) + } + if (!selectedOrg) { + return ui.setNotification({ + category: 'error', + message: 'Unable to approve request: No organization selected', + }) + } + + try { + setIsSubmitting(true) + const res = await approveRequest({ id: auth_id, organization_id: selectedOrg }) + router.push(res.url) + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: `Failed to approve request: ${error.message}`, + }) + setIsSubmitting(false) + } } - // [Joshen] To be replaced with actual API call above - const isApiAuthDetailsError = false - const isExpired = false + const onDeclineRequest = async () => { + if (!auth_id) + return ui.setNotification({ + category: 'error', + message: 'Unable to decline request: auth_id is missing ', + }) + + try { + setIsSubmitting(true) + await declineRequest({ id: auth_id }) + router.push('/projects') + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: `Failed to decline request: ${error.message}`, + }) + setIsSubmitting(false) + } + } + + if (isLoading) { + return ( + Authorize API access

        }> +
        + + + +
        +
        + ) + } if (auth_id === undefined) { return ( @@ -37,9 +109,9 @@ const APIAuthorizationPage: NextPageWithLayout = () => { ) } - if (isApiAuthDetailsError) { + if (isInvalid || isError) { return ( - Authorization for API access

        }> + Authorize API access

        }>
        { ) } + if (isApproved) { + const approvedOrganization = organizations?.find( + (org) => org.slug === requester.approved_organization_slug + ) + + return ( + Authorize API access for {requester?.name}

        }> +
        + +

        + {requester.name} has read and write access to the organization " + {approvedOrganization?.name ?? 'Unknown'}" and all of its projects +

        +

        + Approved on: {dayjs(requester.approved_at).format('DD MMM YYYY HH:mm:ss (ZZ)')} +

        +
        +
        +
        + ) + } + return ( Authorize API access for {apiAuthDetails.name}

        } + header={

        Authorize API access for {requester?.name}

        } footer={
        - -
        @@ -76,23 +174,26 @@ const APIAuthorizationPage: NextPageWithLayout = () => {
        + className="w-8 h-8 md:w-10 md:h-10 bg-center bg-no-repeat bg-cover flex items-center justify-center" + // [Joshen] For when we support icons + // style={{ backgroundImage: `url('${requester?.icon}')` }} + > +

        {requester?.name[0]}

        +

        - {apiAuthDetails.name} is requesting API access to an organization. The application will - be able to{' '} - - read and write the organization's settings and projects + {requester?.name} ({requester?.domain}) is requesting API access to an organization. The + application will be able to{' '} + + read and write the organization's settings and all of its projects.

    {/* Expiry warning */} {isExpired && ( - + Please retry your authorization request from the requesting app )} @@ -104,12 +205,17 @@ const APIAuthorizationPage: NextPageWithLayout = () => {
    ) : ( - + {(organizations ?? []).map((organization) => ( {organization.name} From 0d17d50eb529750f87c0368a05f58ed965b03971 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Fri, 5 May 2023 16:42:04 +0000 Subject: [PATCH 17/70] Update apps/docs/pages/guides/auth/auth-password-reset.mdx Co-authored-by: Greg Richardson --- apps/docs/pages/guides/auth/auth-password-reset.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx index 9bd052a0345..8e769e955b2 100644 --- a/apps/docs/pages/guides/auth/auth-password-reset.mdx +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -20,8 +20,8 @@ the update password page. This url must be saved in your allowed [Redirect URLs] list or it won't work. ```ts -await supabase.auth.resetPasswordForEmail("hello@example.com", { - redirectTo: 'http://example.com/account/update-password' +await supabase.auth.resetPasswordForEmail('hello@example.com', { + redirectTo: 'http://example.com/account/update-password', }) ``` From 45141a745977ebbdae5c1eea38d7d3cf24b169c4 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Fri, 5 May 2023 16:42:18 +0000 Subject: [PATCH 18/70] Update apps/docs/pages/guides/auth/auth-password-reset.mdx Co-authored-by: Greg Richardson --- apps/docs/pages/guides/auth/auth-password-reset.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx index 8e769e955b2..2d56f06c77b 100644 --- a/apps/docs/pages/guides/auth/auth-password-reset.mdx +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -48,8 +48,8 @@ allowed [Redirect URLs](https://app.supabase.com/project/_/auth/url-configuratio redirect the user. ```ts -await supabase.auth.resetPasswordForEmail("hello@example.com", { - redirectTo: 'http://example.com/api/auth/callback?next=/account/update-password' +await supabase.auth.resetPasswordForEmail('hello@example.com', { + redirectTo: 'http://example.com/api/auth/callback?next=/account/update-password', }) ``` From 7673a3c4b02bbe2625e8e94c04d89b13ada08f91 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Fri, 5 May 2023 16:42:32 +0000 Subject: [PATCH 19/70] Update apps/docs/pages/guides/auth/auth-password-reset.mdx Co-authored-by: Greg Richardson --- apps/docs/pages/guides/auth/auth-password-reset.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx index 2d56f06c77b..b5625e09047 100644 --- a/apps/docs/pages/guides/auth/auth-password-reset.mdx +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -55,8 +55,7 @@ await supabase.auth.resetPasswordForEmail('hello@example.com', { -We are using `next` as our query parameter but this is just personal preference, you could -use a different query parameter name. +We are using `next` as our query parameter, but this can name whatever you like. From 899921de63d54876008731dcfd31830193995d4a Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Fri, 5 May 2023 16:42:44 +0000 Subject: [PATCH 20/70] Update apps/docs/pages/guides/auth/auth-password-reset.mdx Co-authored-by: Greg Richardson --- apps/docs/pages/guides/auth/auth-password-reset.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx index b5625e09047..217dcf9fb2a 100644 --- a/apps/docs/pages/guides/auth/auth-password-reset.mdx +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -60,7 +60,7 @@ We are using `next` as our query parameter, but this can name whatever you like. ### Email link -The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the code exchange code. +The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the exchange code. ### Exchange authorization code After getting redirected to the server page we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function. From 296d4bdfc1d8b0806a44a00b5bba3bfba775a90e Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Fri, 5 May 2023 16:42:57 +0000 Subject: [PATCH 21/70] Update apps/docs/pages/guides/auth/auth-password-reset.mdx Co-authored-by: Greg Richardson --- apps/docs/pages/guides/auth/auth-password-reset.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx index 217dcf9fb2a..aef61326399 100644 --- a/apps/docs/pages/guides/auth/auth-password-reset.mdx +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -63,7 +63,7 @@ We are using `next` as our query parameter, but this can name whatever you like. The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the exchange code. ### Exchange authorization code -After getting redirected to the server page we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function. +After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function. ```ts // api/auth/callback.ts From 7bef3f1cd0eb2e152667a2a356d7a053e2480d9c Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Fri, 5 May 2023 16:44:15 +0000 Subject: [PATCH 22/70] Update apps/docs/pages/guides/auth/auth-password-reset.mdx Co-authored-by: Greg Richardson --- apps/docs/pages/guides/auth/auth-password-reset.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx index aef61326399..85cf47c82e5 100644 --- a/apps/docs/pages/guides/auth/auth-password-reset.mdx +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -67,8 +67,8 @@ After redirecting to the server page, we need to retrieve the code from the quer ```ts // api/auth/callback.ts -// code can be retrieved using your framework of choice way of getting query parameter from the url. -// This is just example code +// The code is retrieved from the query parameter - use whichever mechanism is recommended +// for your app/framework. This is just an example. const code = url.searchParams.get('code') // call the Supabase API to exchange the code for a session From 026c0633753acb047f4a1fe29f66e7d7d8036921 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Fri, 5 May 2023 16:44:23 +0000 Subject: [PATCH 23/70] Update apps/docs/pages/guides/auth/auth-password-reset.mdx Co-authored-by: Greg Richardson --- apps/docs/pages/guides/auth/auth-password-reset.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/auth/auth-password-reset.mdx b/apps/docs/pages/guides/auth/auth-password-reset.mdx index 85cf47c82e5..03ff57c24a3 100644 --- a/apps/docs/pages/guides/auth/auth-password-reset.mdx +++ b/apps/docs/pages/guides/auth/auth-password-reset.mdx @@ -85,8 +85,8 @@ We will also need to check for the `next` query parameter to redirect the user t ```ts // api/auth/callback.ts -// password page path can be retrieved using your framework of choice way of getting query parameter from the url. -// This is just example code +// The password page path is retrieved from the query parameter - use whichever mechanism is recommended +// for your app/framework. This is just an example. const next = url.searchParams.get('next') // using NextJS response object in this example From ffcfca5f552bbb4122a20f8c7d11018bd77e5aa6 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sat, 6 May 2023 01:13:38 +0800 Subject: [PATCH 24/70] fix: add exchangeCodeForSession to spec --- spec/supabase_js_v2.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/supabase_js_v2.yml b/spec/supabase_js_v2.yml index 780e982f411..b7c659d38f6 100644 --- a/spec/supabase_js_v2.yml +++ b/spec/supabase_js_v2.yml @@ -567,6 +567,20 @@ functions: if (event == 'USER_UPDATED') console.log('USER_UPDATED', session) }) ``` + - id: exchange-code-for-session + title: 'exchangeCodeForSession()' + $ref: '@supabase/gotrue-js.GoTrueClient.exchangeCodeForSession' + notes: | + - Used when `flowType` is set to True in client options. + examples: + - id: exchange-auth-code + name: Exchange Auth Code + isSpotlight: true + code: | + ```js + supabase.auth.exchangeCodeForSession('34e770dd-9ff9-416c-87fa-43b31d7ef225') + ``` + - id: mfa-enroll title: 'mfa.enroll()' $ref: '@supabase/gotrue-js.GoTrueMFAApi.enroll' From 30adf466f00af4188b4b6b42c50ca91640f050db Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sat, 6 May 2023 01:18:05 +0800 Subject: [PATCH 25/70] fix: update client spec --- spec/common-client-libs-sections.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/common-client-libs-sections.json b/spec/common-client-libs-sections.json index 06bacd153c6..52b654279d2 100644 --- a/spec/common-client-libs-sections.json +++ b/spec/common-client-libs-sections.json @@ -558,6 +558,14 @@ "product": "auth", "type": "function" }, + { + "id": "exchange-code-for-session", + "title": "Exchange an auth code for a session", + "slug": "auth-exchangecodeforsession", + "product": "auth", + "type": "function" + + }, { "id": "mfa-enroll", "title": "Enroll a factor", From d73dabe08235d6fc923794f75e4a5335343914c4 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Fri, 5 May 2023 16:12:14 -0230 Subject: [PATCH 26/70] Move home sidebar icons to own component --- .../components/Navigation/Navigation.types.ts | 11 + .../Navigation/NavigationMenu/HomeMenu.tsx | 151 +--------- .../NavigationMenu/HomeMenuIconPicker.tsx | 75 +++++ .../NavigationMenu/HomeMenuIcons.tsx | 258 ++++++++++++++++++ .../NavigationMenu.constants.ts | 143 +++++++++- .../NavigationMenuGuideListItems.tsx | 21 +- 6 files changed, 492 insertions(+), 167 deletions(-) create mode 100644 apps/docs/components/Navigation/NavigationMenu/HomeMenuIconPicker.tsx create mode 100644 apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx diff --git a/apps/docs/components/Navigation/Navigation.types.ts b/apps/docs/components/Navigation/Navigation.types.ts index 76afaa7b1c6..a644cc7c70d 100644 --- a/apps/docs/components/Navigation/Navigation.types.ts +++ b/apps/docs/components/Navigation/Navigation.types.ts @@ -22,3 +22,14 @@ export interface References { currentVersion?: string } } + +type MenuItem = { + label: string + icon?: string + href?: string + level?: string + hasLightIcon?: boolean + community?: boolean +} + +export type HomepageMenuItems = MenuItem[][] diff --git a/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx index db93e1616a2..5298b6a31a8 100644 --- a/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx @@ -5,145 +5,10 @@ import { useRouter } from 'next/router' import { Fragment } from 'react' import { Badge } from '~/../../packages/ui' -const home = [ - [ - { - label: 'Home', - icon: '/img/icons/menu/home', - href: '/', - level: 'home', - }, - { - label: 'Getting Started', - icon: '/img/icons/menu/getting-started', - href: '/guides/getting-started', - level: 'gettingstarted', - }, - ], - [ - { - label: 'Database', - icon: '/img/icons/menu/database', - href: '/guides/database', - level: 'database', - }, - { - label: 'Serverless APIs', - icon: '/img/icons/menu/reference', - href: '/guides/api', - level: 'api', - }, - { - label: 'Auth', - icon: '/img/icons/menu/auth', - href: '/guides/auth', - level: 'auth', - }, - { - label: 'Edge Functions', - icon: '/img/icons/menu/functions', - href: '/guides/functions', - level: 'functions', - }, - { - label: 'Realtime', - icon: '/img/icons/menu/realtime', - href: '/guides/realtime', - level: 'realtime', - }, - { - label: 'Storage', - icon: '/img/icons/menu/storage', - href: '/guides/storage', - level: 'storage', - }, - ], - [ - { - label: 'Platform', - icon: '/img/icons/menu/platform', - href: '/guides/platform', - level: 'platform', - }, - { - label: 'Resources', - icon: '/img/icons/menu/platform', - href: '/guides/resources', - level: 'resources', - }, - { - label: 'Self-Hosting', - icon: '/img/icons/menu/platform', - href: '/guides/self-hosting', - level: 'self_hosting', - }, - { - label: 'Integrations', - icon: '/img/icons/menu/integrations', - hasLightIcon: true, - href: '/guides/integrations', - level: 'integrations', - }, - ], - [ - { - label: 'Client Library Reference', - }, - { - label: 'JavaScript', - icon: '/img/icons/menu/reference-javascript', - href: '/reference/javascript/introduction', - level: 'reference_javascript', - }, - { - label: 'Flutter', - icon: '/img/icons/menu/reference-dart', - href: '/reference/dart/introduction', - level: 'reference_dart', - }, - { - label: 'Python', - icon: '/img/icons/menu/reference-python', - href: '/reference/python/introduction', - level: 'reference_python', - community: true, - }, - { - label: 'C#', - icon: '/img/icons/menu/reference-csharp', - href: '/reference/csharp/introduction', - level: 'reference_csharp', - community: true, - }, - { - label: 'Swift', - icon: '/img/icons/menu/reference-swift', - href: '/reference/swift/introduction', - level: 'reference_swift', - community: true, - }, - { - label: 'Tools', - }, - { - label: 'Management API', - icon: '/img/icons/menu/reference-api', - href: '/reference/api/introduction', - level: 'reference_javascript', - }, - { - label: 'Supabase CLI', - icon: '/img/icons/menu/reference-cli', - href: '/guides/cli', - level: 'reference_javascript', - }, - ], -] +import { HOMEPAGE_MENU_ITEMS } from './NavigationMenu.constants' +import HomeMenuIconPicker from './HomeMenuIconPicker' const NavigationMenuHome = ({ active }) => { - const router = useRouter() - const { isDarkMode } = useTheme() - return (
    { ].join(' ')} >