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/lib/fetchWrappers.tsx b/apps/docs/lib/fetchWrappers.tsx index c573a0d37dc..0313e49565f 100644 --- a/apps/docs/lib/fetchWrappers.tsx +++ b/apps/docs/lib/fetchWrappers.tsx @@ -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/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index 8563418f9a8..7c3519884b8 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -1,6 +1,6 @@ import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider } from '@supabase/auth-helpers-react' -import { AuthProvider, ThemeProvider } from 'common' +import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import ReactMarkdown from 'react-markdown' @@ -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' @@ -19,16 +19,21 @@ import '../styles/prism-okaidia.scss' function MyApp({ Component, pageProps }: AppPropsWithLayout) { const router = useRouter() + const telemetryProps = useTelemetryProps() const [supabase] = useState(() => IS_PLATFORM || LOCAL_SUPABASE ? createBrowserSupabaseClient() : undefined ) 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, + ga: { + screen_resolution: telemetryProps?.screenResolution, + language: telemetryProps?.language, + }, }) } diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx index aa420134d55..d4b0805ec49 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx @@ -45,21 +45,21 @@ These variables will be exposed on the browser, and that's completely fine since [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. ```ts title=lib/supabase.ts -import * as SecureStore from "expo-secure-store"; +import 'react-native-url-polyfill/auto' +import * as SecureStore from 'expo-secure-store' import { createClient } from '@supabase/supabase-js' - const ExpoSecureStoreAdapter = { getItem: (key: string) => { - return SecureStore.getItemAsync(key); + return SecureStore.getItemAsync(key) }, setItem: (key: string, value: string) => { - SecureStore.setItemAsync(key, value); + SecureStore.setItemAsync(key, value) }, removeItem: (key: string) => { - SecureStore.deleteItemAsync(key); + SecureStore.deleteItemAsync(key) }, -}; +} const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY diff --git a/apps/www/_customers/mendableai.mdx b/apps/www/_customers/mendableai.mdx index 54f35d8b054..70fa0b0075b 100644 --- a/apps/www/_customers/mendableai.mdx +++ b/apps/www/_customers/mendableai.mdx @@ -1,8 +1,8 @@ --- title: Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings. name: Mendable.ai -description: How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector. -author: rory_wilding +description: How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector. +author: paul_copplestone author_title: Supabase author_url: https://github.com/kiwicopple author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4 @@ -24,31 +24,35 @@ misc: [{ label: 'Backed by', text: 'Y Combinator' }] about: Mendable.ai is Chat Powered Search for Documentation. --- -[Mendable.ai](http://mendable.ai/) provides a chat-powered search engine for technical documentation. Their AI-powered search tool makes it easier for developers and other technical users to find relevant information in complex documentation. Users can simply ask questions in natural language, and the tool returns the most relevant answers. [Mendable.ai](http://mendable.ai/)'s search engine also provides detailed analytics, which helps teams identify knowledge gaps and areas for improvement in their documentation. +[Mendable.ai](http://mendable.ai/) provides a chat-powered search engine for technical documentation. Their AI-powered search tool makes it easier for developers and other technical users to find relevant information in complex documentation. Users can simply ask questions in natural language, and the tool returns the most relevant answers. Mendable.ai's search engine also provides detailed analytics, which helps teams identify knowledge gaps and areas for improvement in their documentation. ## The Challenge -[Mendable.ai](http://mendable.ai/) needed to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations for their Chat Powered Search for Documentation. They tried Faiss, Weviate, and Pinecone., but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors. +Mendable.ai was experiencing tremendous success, with WAUs growing nearly 300% since March, and started looking for a tool to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations for their Chat Powered Search for Documentation. They tried Faiss, Weviate, and Pinecone, but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors. Why they chose Supabase: -[Mendable.ai](http://mendable.ai/) discovered that Supabase supports pg_vector and found it to be a simple and cost-effective solution. They were impressed with the open-source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use. +Mendable.ai discovered that Supabase supports pg_vector and found it to be a simple and cost-effective solution. They were impressed with the open-source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use. - We tried other vector databases - we tried Faiss, we tried Weviate, we tried Pinecone. We found them to be incredibly expensive and not very intuitive. If you’re just doing vector search they’re great, but if you need to store a bunch of metadata that becomes a huge pain. + We tried other vector databases - we tried Faiss, we tried Weviate, we tried Pinecone. We found + them to be incredibly expensive and not very intuitive. If you’re just doing vector search they’re + great, but if you need to store a bunch of metadata that becomes a huge pain. ## What They Built -Using Supabase's pg_vector, [Mendable.ai](http://mendable.ai/) was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, [Mendable.ai](http://mendable.ai/) was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost. +Using Supabase's pg_vector, Mendable.ai was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, Mendable.ai was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost. ## The Results -Thanks to Supabase's pg_vector, [Mendable.ai](http://mendable.ai/) was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack. +Thanks to Supabase's pg_vector, Mendable.ai was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack. ## Tech stack -[Mendable.ai](http://mendable.ai/)'s tech stack includes React, Next.js, Express, Vercel, and Supabase. +Mendable.ai's tech stack includes React, Next.js, Express, Vercel, and Supabase. - We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for all the ways we need use it, Supabase has been just as performant - if not more performant - than the other vector databases. + We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for + all the ways we need use it, Supabase has been just as performant - if not more performant - than + the other vector databases. diff --git a/apps/www/components/Features/index.tsx b/apps/www/components/Features/index.tsx index b5c8dc3f68f..ab6510718e5 100644 --- a/apps/www/components/Features/index.tsx +++ b/apps/www/components/Features/index.tsx @@ -1,12 +1,52 @@ -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 Telemetry from '~/lib/telemetry' +import gaEvents from '~/lib/gaEvents' import SectionContainer from '../Layouts/SectionContainer' import ProductIcon from '../ProductIcon' import TextLink from '../TextLink' +import { useTelemetryProps } from 'common/hooks/useTelemetryProps' +import { useRouter } from 'next/router' const Features = () => { + const router = useRouter() + const telemetryProps = useTelemetryProps() + + const sendTelemetryEvent = async (product: any) => { + switch (product) { + case 'Database': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_database'], + telemetryProps, + router + ) + case 'Authentication': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_auth'], + telemetryProps, + router + ) + case 'Storage': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_storage'], + telemetryProps, + router + ) + case 'Edge Functions': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_edgeFunctions'], + telemetryProps, + router + ) + case 'Realtime': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_realtime'], + telemetryProps, + router + ) + } + } + const IconSections = Object.values(Solutions).map((solution: any) => { const { name, description, icon, label, url } = solution if (solution.name === 'Realtime') return null @@ -24,7 +64,13 @@ const Features = () => { {label} )} - {url && } + {url && ( + sendTelemetryEvent(name)} + /> + )} ) }) diff --git a/apps/www/components/Hero.tsx b/apps/www/components/Hero.tsx index e97cd31fa74..d2decb0ab08 100644 --- a/apps/www/components/Hero.tsx +++ b/apps/www/components/Hero.tsx @@ -1,10 +1,19 @@ -import { Button, IconBookOpen, Space } from 'ui' -import Link from 'next/link' +import { Button, IconBookOpen } from 'ui' import { useRouter } from 'next/router' +import Link from 'next/link' import SectionContainer from './Layouts/SectionContainer' +import Telemetry, { TelemetryEvent } from '~/lib/telemetry' +import gaEvents from '~/lib/gaEvents' +import { useTelemetryProps } from 'common/hooks/useTelemetryProps' const Hero = () => { - const { basePath } = useRouter() + const router = useRouter() + const { basePath } = router + const telemetryProps = useTelemetryProps() + + const sendTelemetryEvent = async (event: TelemetryEvent) => { + await Telemetry.sendEvent(event, telemetryProps, router) + } return (
@@ -40,14 +49,16 @@ const Hero = () => {
- + sendTelemetryEvent(gaEvents['www_hp_hero_startProject'])}> - + sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])} + > diff --git a/apps/www/components/TextLink.tsx b/apps/www/components/TextLink.tsx index 3bd3edfc10e..73d54f6136e 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} -
- + + +
+ {`${label} about ${url}`} + {label} +
+ +
-
- + + ) } diff --git a/apps/www/data/CustomerStories.json b/apps/www/data/CustomerStories.json index d97f6e79991..c55ae5a858e 100644 --- a/apps/www/data/CustomerStories.json +++ b/apps/www/data/CustomerStories.json @@ -1,4 +1,21 @@ [ + { + "type": "Customer Story", + "title": "Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.", + "description": "How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.", + "imgUrl": "images/customers/logos/mendableai.png", + "logoUrl": "images/customers/logos/mendableai.png", + "organization": "Mendable.ai", + "url": "/customers/mendableai", + "path": "/customers/mendableai", + "postMeta": { + "name": "Paul Copplestone", + "avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4", + "publishDate": "Feb 16, 2023", + "readLength": 6 + }, + "ctaText": "View story" + }, { "type": "Customer Story", "title": "HappyTeams unlocks better performance and reduces cost with Supabase.", diff --git a/apps/www/data/Solutions.json b/apps/www/data/Solutions.json index f12ff0f89fa..823d5dd261d 100644 --- a/apps/www/data/Solutions.json +++ b/apps/www/data/Solutions.json @@ -23,14 +23,6 @@ "label": "", "url": "/storage" }, - "realtime": { - "name": "Realtime", - "icon": "M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zM12 2.25V4.5m5.834.166l-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243l-1.59-1.59", - "description": "Store, organize, and serve large files. Any media, including videos and images.", - "description_short": "", - "label": "", - "url": "/storage" - }, "edge-functions": { "name": "Edge Functions", "icon": "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4", diff --git a/apps/www/lib/constants.ts b/apps/www/lib/constants.ts index e3c815b5dca..445c0a6810a 100644 --- a/apps/www/lib/constants.ts +++ b/apps/www/lib/constants.ts @@ -1,5 +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 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/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..a9db7dead20 --- /dev/null +++ b/apps/www/lib/gaEvents.ts @@ -0,0 +1,45 @@ +import { TelemetryEvent } from './telemetry' + +interface TelemetryEventType { + [key: string]: TelemetryEvent +} + +const TelemetryEvents: TelemetryEventType = { + www_hp_hero_startProject: { + action: 'www_hp_hero_startProject', + category: 'link', + label: '', + }, + www_hp_hero_documentation: { + action: 'www_hp_hero_documentation', + 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 TelemetryEvents diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 5c57d2f3456..db6e50c2f8f 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -1913,4 +1913,57 @@ module.exports = [ source: '/docs/guides/cli/local-development', destination: '/docs/guides/getting-started/local-development', }, + + // old case studies moved to /customers + + { + permanent: true, + source: '/blog/case-study-monitoro', + destination: '/customers', + }, + { + permanent: true, + source: '/blog/case-study-tayfa', + destination: '/customers', + }, + { + permanent: true, + source: '/blog/case-study-xendit', + destination: '/customers/xendit', + }, + { + permanent: true, + source: '/blog/case-study-roboflow', + destination: '/customers', + }, + { + permanent: true, + source: '/blog/toad-a-link-shortener-with-simple-apis-for-low-coders', + destination: '/customers', + }, + { + permanent: true, + source: '/blog/epsilon3-self-hosting', + destination: '/customers/epsilon3', + }, + { + permanent: true, + source: '/blog/spot-flutter-with-postgres', + destination: '/customers', + }, + { + permanent: true, + source: '/blog/mobbin-supabase-200000-users', + destination: '/customers/mobbin', + }, + { + permanent: true, + source: '/blog/replenysh-time-to-value-in-less-than-24-hours', + destination: '/customers/replenysh', + }, + { + permanent: true, + source: '/blog/how-supabase-accelerates-development-of-all-pull-together', + destination: '/customers', + }, ] diff --git a/apps/www/lib/telemetry.ts b/apps/www/lib/telemetry.ts new file mode 100644 index 00000000000..a880edb6f5b --- /dev/null +++ b/apps/www/lib/telemetry.ts @@ -0,0 +1,42 @@ +import { post } from '~/lib/fetchWrapper' +import { API_URL, IS_PROD, IS_PREVIEW } from 'lib/constants' +import { NextRouter } from 'next/router' + +export interface TelemetryEvent { + category: string + action: string + label: string + value?: string +} + +export interface TelemetryProps { + 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: TelemetryEvent, gaProps: TelemetryProps, router: NextRouter) => { + if (!IS_PROD && !IS_PREVIEW) return + + const { category, action, label, value } = event + + return post(`${API_URL}/telemetry/event`, { + action: action, + category: category, + label: label, + value: value, + page_referrer: document?.referrer, + page_title: document?.title, + page_location: router.asPath, + 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..7329be0174e 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -1,22 +1,27 @@ -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' import { useEffect } from 'react' import Meta from '~/components/Favicons' import '../styles/index.css' -import { post } from './../lib/fetchWrapper' -import { AuthProvider, ThemeProvider } from 'common' +import { post } from '~/lib/fetchWrapper' +import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common' import Head from 'next/head' export default function MyApp({ Component, pageProps }: AppProps) { const router = useRouter() + const telemetryProps = useTelemetryProps() 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, + ga: { + screen_resolution: telemetryProps?.screenResolution, + language: telemetryProps?.language, + }, }) } diff --git a/apps/www/public/customers-rss.xml b/apps/www/public/customers-rss.xml index e8c128abd81..6835a30b98e 100644 --- a/apps/www/public/customers-rss.xml +++ b/apps/www/public/customers-rss.xml @@ -5,42 +5,49 @@ https://supabase.com Latest news from Supabase en - Wed, 15 Feb 2023 03:03:30 +0000 + Thu, 04 May 2023 22:10:00 +0000 + https://supabase.com/customers/mendableai + Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings. + https://supabase.com/customers/mendableai + How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector. + Thu, 04 May 2023 22:10:00 +0000 + + https://supabase.com/customers/happyteams HappyTeams unlocks better performance and reduces cost with Supabase. https://supabase.com/customers/happyteams How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back. - Wed, 15 Feb 2023 03:03:30 +0000 + Wed, 15 Feb 2023 23:11:00 +0000 https://supabase.com/customers/xendit Xendit use Supabase and create a full solution shipped to production in less than one week. https://supabase.com/customers/xendit As a payment processor, Xendit are responsible for verifying that all transactions are legal. - Mon, 13 Feb 2023 03:03:30 +0000 + Mon, 13 Feb 2023 23:11:00 +0000 https://supabase.com/customers/replenysh Replenysh uses Supabase to implement OTP in less than 24 hours. https://supabase.com/customers/replenysh With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres. - Mon, 13 Feb 2023 03:03:30 +0000 + Mon, 13 Feb 2023 23:11:00 +0000 https://supabase.com/customers/mobbin How Mobbin migrated 200,000 users from Firebase for a better authentication experience. https://supabase.com/customers/mobbin Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps. - Mon, 13 Feb 2023 03:03:30 +0000 + Mon, 13 Feb 2023 23:11:00 +0000 https://supabase.com/customers/epsilon3 Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations. https://supabase.com/customers/epsilon3 Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions. - Mon, 13 Feb 2023 03:03:30 +0000 + Mon, 13 Feb 2023 23:11:00 +0000 diff --git a/package-lock.json b/package-lock.json index 0881d78353b..9ed78cb140f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68107,7 +68107,7 @@ "@types/react": "^17.0.37", "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "^17.0.11", - "@types/react-syntax-highlighter": "*", + "@types/react-syntax-highlighter": "^15.5.6", "autoprefixer": "^10.4.2", "clsx": "^1.2.1", "cmdk-supabase": "^0.2.2", diff --git a/packages/common/hooks/index.ts b/packages/common/hooks/index.ts index 7560f7ebcf5..65b5a76ec98 100644 --- a/packages/common/hooks/index.ts +++ b/packages/common/hooks/index.ts @@ -1,2 +1,3 @@ export * from './useBreakpoint' export * from './useParams' +export * from './useTelemetryProps' diff --git a/packages/common/hooks/useTelemetryProps.ts b/packages/common/hooks/useTelemetryProps.ts new file mode 100644 index 00000000000..73e63cd5e4f --- /dev/null +++ b/packages/common/hooks/useTelemetryProps.ts @@ -0,0 +1,12 @@ +import { useRouter } from 'next/router' + +const isBrowser = typeof window !== 'undefined' + +export function useTelemetryProps() { + const { locale } = useRouter() + + return { + screenResolution: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined, + language: locale ?? 'en-US', + } +} 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", diff --git a/spec/supabase_js_v2.yml b/spec/supabase_js_v2.yml index 780e982f411..2b06ecc41f8 100644 --- a/spec/supabase_js_v2.yml +++ b/spec/supabase_js_v2.yml @@ -400,6 +400,9 @@ functions: - id: get-session title: 'getSession()' $ref: '@supabase/gotrue-js.GoTrueClient.getSession' + notes: | + - This method retrieves the current local session (i.e local storage). + - If the session has an expired access token, this method will use the refresh token to get a new session. examples: - id: get-the-session-data name: Get the session data @@ -412,8 +415,8 @@ functions: title: 'getUser()' $ref: '@supabase/gotrue-js.GoTrueClient.getUser' notes: | - - This method gets the user object from the current session. - - Fetches the user object from the database instead of local session. + - This method fetches the user object from the database instead of local session. + - This method is useful for checking if the user is authorized because it validates the user's access token JWT on the server. - Should be used only when you require the most current user data. For faster results, `getSession().session.user` is recommended. examples: - id: get-the-logged-in-user-with-the-current-existing-session @@ -486,7 +489,7 @@ functions: title: 'refreshSession()' $ref: '@supabase/gotrue-js.GoTrueClient.refreshSession' notes: | - - This method will refresh the session whether the current one is expired or not. + - This method will refresh and return a new session whether the current one is expired or not. examples: - id: refresh-session-using-the-current-session name: Refresh session using the current session @@ -567,6 +570,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 `pkce` 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' diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts index e20cebbba26..b52e11f899b 100644 --- a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts @@ -8,6 +8,7 @@ export interface TaxId { code: string country: string placeholder: string + vatPrefix?: string } export interface StripeTaxId { @@ -30,6 +31,7 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Austria', placeholder: 'ATU12345678', + vatPrefix: 'ATU', }, { name: 'AU ABN', @@ -48,12 +50,14 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Belgium', placeholder: 'BE0123456789', + vatPrefix: 'BE', }, { name: 'BG VAT', code: 'eu_vat', country: 'Bulgaria', placeholder: 'BG0123456789', + vatPrefix: 'BG', }, { name: 'BR CNPJ', @@ -108,6 +112,7 @@ export const TAX_IDS: TaxId[] = [ code: 'ch_vat', country: 'Switzerland', placeholder: 'CHE-123.456.789 MWST', + vatPrefix: 'CHE', }, { name: 'CL TIN', @@ -120,30 +125,35 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Cyprus', placeholder: 'CY12345678Z', + vatPrefix: 'CY', }, { name: 'CZ VAT', code: 'eu_vat', country: 'Czech Republic', placeholder: 'CZ1234567890', + vatPrefix: 'CZ', }, { name: 'DE VAT', code: 'eu_vat', country: 'Germany', placeholder: 'DE123456789', + vatPrefix: 'DE', }, { name: 'DK VAT', code: 'eu_vat', country: 'Denmark', placeholder: 'DK12345678', + vatPrefix: 'DK', }, { name: 'EE VAT', code: 'eu_vat', country: 'Estonia', placeholder: 'EE123456789', + vatPrefix: 'EE', }, { name: 'ES CIF', @@ -155,25 +165,29 @@ export const TAX_IDS: TaxId[] = [ name: 'ES VAT', code: 'eu_vat', country: 'Spain', - placeholder: 'ESA1234567Z', + placeholder: 'ESX1234567Z', + vatPrefix: 'ES', }, { name: 'FI VAT', code: 'eu_vat', country: 'Finland', placeholder: 'FI12345678', + vatPrefix: 'FI', }, { name: 'FR VAT', code: 'eu_vat', country: 'France', placeholder: 'FRAB123456789', + vatPrefix: 'FR', }, { name: 'GB VAT', code: 'eu_vat', country: 'United Kingdom', placeholder: 'GB123456789', + vatPrefix: 'GB', }, { name: 'GE VAT', @@ -186,6 +200,7 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Greece', placeholder: 'EL123456789', + vatPrefix: 'EL', }, { name: 'HK BR', @@ -198,12 +213,14 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Croatia', placeholder: 'HR12345678912', + vatPrefix: 'HR', }, { name: 'HU VAT', code: 'eu_vat', country: 'Hungary', placeholder: 'HU12345678912', + vatPrefix: 'HU', }, { name: 'ID NPWP', @@ -216,6 +233,7 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Ireland', placeholder: 'IE1234567AB', + vatPrefix: 'IE', }, { name: 'IL VAT', @@ -240,6 +258,7 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Italy', placeholder: 'IT12345678912', + vatPrefix: 'IT', }, { name: 'JP CN', @@ -270,24 +289,28 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Lithuania', placeholder: 'LT123456789123', + vatPrefix: 'LT', }, { name: 'LU VAT', code: 'eu_vat', country: 'Luxembourg', placeholder: 'LU12345678', + vatPrefix: 'LU', }, { name: 'LV VAT', code: 'eu_vat', country: 'Latvia', placeholder: 'LV12345678912', + vatPrefix: 'LV', }, { name: 'MT VAT', code: 'eu_vat', country: 'Malta', placeholder: 'MT12345678', + vatPrefix: 'MT', }, { name: 'MX RFC', @@ -318,6 +341,7 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Netherlands', placeholder: 'NL123456789B12', + vatPrefix: 'NL', }, { name: 'NO VAT', @@ -336,18 +360,21 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Poland', placeholder: 'PL1234567890', + vatPrefix: 'PL', }, { name: 'PT VAT', code: 'eu_vat', country: 'Portugal', placeholder: 'PT123456789', + vatPrefix: 'PT', }, { name: 'RO VAT', code: 'eu_vat', country: 'Romania', placeholder: 'RO1234567891', + vatPrefix: 'RO', }, { name: 'RU INN', @@ -372,6 +399,7 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Sweden', placeholder: 'SE123456789123', + vatPrefix: 'SE', }, { name: 'SG GST', @@ -390,12 +418,14 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'Slovenia', placeholder: 'SI12345678', + vatPrefix: 'SI', }, { name: 'SK VAT', code: 'eu_vat', country: 'Slovakia', placeholder: 'SK1234567891', + vatPrefix: 'SK', }, { name: 'TH VAT', @@ -426,6 +456,7 @@ export const TAX_IDS: TaxId[] = [ code: 'eu_vat', country: 'United Kingdom (Northern Ireland)', placeholder: 'XI123456789', + vatPrefix: 'XI', }, { name: 'ZA VAT', diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts index f30e7ca3b5c..90a37a324e0 100644 --- a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts @@ -11,12 +11,11 @@ export const sanitizeTaxID = (taxId: StripeTaxId) => { const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name) - // The prefix of the country name (ex: "BE" for Belgium, "ESA" for Spain)") - const countryPrefix = selectedTaxId?.placeholder.match(/[a-z]+|[^a-z]+/gi)?.[0] + const vatIdPrefix = selectedTaxId?.vatPrefix - // if the value doesn't start with the country code, prepend them - if (taxId.type === 'eu_vat' && countryPrefix && !taxId.value.startsWith(countryPrefix)) { - newID.value = `${countryPrefix}${taxId.value}` + // if the value doesn't start with the prefix, prepend them + if (vatIdPrefix && !taxId.value.startsWith(vatIdPrefix)) { + newID.value = `${vatIdPrefix}${taxId.value}` } return newID diff --git a/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx b/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx index 43548b7ec7d..86f850fbce4 100644 --- a/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx +++ b/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx @@ -8,7 +8,7 @@ import { useProfileQuery } from 'data/profile/profile-query' import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.constants' import SQLCard from './SQLCard' import { createSqlSnippetSkeleton } from '../SQLEditor.utils' -import { useParams } from 'common' +import { useTelemetryProps, useParams } from 'common' import { useRouter } from 'next/router' import { SqlSnippet } from 'data/content/sql-snippets-query' import { useSqlEditorStateSnapshot } from 'state/sql-editor' @@ -21,6 +21,7 @@ const SQLTemplates = observer(() => { const { data: profile } = useProfileQuery() const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' }) + const telemetryProps = useTelemetryProps() const snap = useSqlEditorStateSnapshot() const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'sql', owner_id: profile?.id }, @@ -77,7 +78,8 @@ const SQLTemplates = observer(() => { action: 'script_clicked', label: x.title, }, - ui.googleAnalyticsProps + telemetryProps, + router ) }} /> @@ -111,7 +113,8 @@ const SQLTemplates = observer(() => { action: 'quickstart_clicked', label: x.title, }, - ui.googleAnalyticsProps + telemetryProps, + router ) }} /> diff --git a/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx b/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx index 2ba8e6a3324..50565d6c697 100644 --- a/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx +++ b/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx @@ -9,6 +9,8 @@ import { CSVLink } from 'react-csv' import { useSqlEditorStateSnapshot } from 'state/sql-editor' // @ts-ignore import MarkdownTable from 'markdown-table' +import { useTelemetryProps } from 'common' +import { useRouter } from 'next/router' export type ResultsDropdownProps = { id: string @@ -17,9 +19,11 @@ export type ResultsDropdownProps = { const ResultsDropdown = ({ id }: ResultsDropdownProps) => { const { project } = useProjectContext() const snap = useSqlEditorStateSnapshot() + const telemetryProps = useTelemetryProps() const result = snap.results?.[id]?.[0] ?? undefined const { ui } = useStore() const csvRef = useRef(null) + const router = useRouter() const csvData = useMemo( () => (result?.rows ? compact(Array.from(result.rows || [])) : ''), @@ -30,7 +34,8 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => { csvRef.current?.link.click() Telemetry.sendEvent( { category: 'sql_editor', action: 'sql_download_csv', label: '' }, - ui.googleAnalyticsProps + telemetryProps, + router ) } @@ -53,7 +58,8 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => { ui.setNotification({ category: 'success', message: 'Copied results to clipboard' }) Telemetry.sendEvent( { category: 'sql_editor', action: 'sql_copy_as_markdown', label: '' }, - ui.googleAnalyticsProps + telemetryProps, + router ) }) } diff --git a/studio/components/layouts/APIAuthorizationLayout.tsx b/studio/components/layouts/APIAuthorizationLayout.tsx new file mode 100644 index 00000000000..143dd4a6526 --- /dev/null +++ b/studio/components/layouts/APIAuthorizationLayout.tsx @@ -0,0 +1,48 @@ +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 +
+
+
+
+
+ +
+ {children} +
+
+ + ) +} + +export default APIAuthorizationLayout diff --git a/studio/components/ui/PageTelemetry.tsx b/studio/components/ui/PageTelemetry.tsx index 412336e28d0..f6de24e69b6 100644 --- a/studio/components/ui/PageTelemetry.tsx +++ b/studio/components/ui/PageTelemetry.tsx @@ -1,4 +1,4 @@ -import { useStore } from 'hooks' +import { useTelemetryProps } from 'common' import { post } from 'lib/common/fetch' import { API_URL, IS_PLATFORM } from 'lib/constants' import { observer } from 'mobx-react-lite' @@ -25,7 +25,7 @@ function sanitizePageViewRoute(_route?: string) { const PageTelemetry: FC = ({ children }) => { const router = useRouter() - const { ui } = useStore() + const telemetryProps = useTelemetryProps() useEffect(() => { function handleRouteChange(url: string) { @@ -71,8 +71,8 @@ const PageTelemetry: FC = ({ children }) => { title: document.title, route, ga: { - screen_resolution: ui.googleAnalyticsProps?.screenResolution, - language: ui.googleAnalyticsProps?.language, + screen_resolution: telemetryProps?.screenResolution, + language: telemetryProps?.language, }, }) } 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..5ac01111adc --- /dev/null +++ b/studio/data/api-authorization/api-authorization-approve-mutation.ts @@ -0,0 +1,42 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' + +export type ApiAuthorizationApproveVariables = { + id: string + organization_id: string +} + +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/authorizations/${id}?skip_browser_redirect=true`, + { organization_id } + ) + if (response.error) throw response.error + return response as ApiAuthorizationApproveResponse +} + +type ApiAuthorizationApproveData = Awaited> + +export const useApiAuthorizationApproveMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => approveApiAuthorization(vars), + 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..7825bb01688 --- /dev/null +++ b/studio/data/api-authorization/api-authorization-decline-mutation.ts @@ -0,0 +1,34 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { delete_ } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' + +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/authorizations/${id}`, {}) + if (response.error) throw response.error + return response as ApiAuthorizationDeclineResponse +} + +type ApiAuthorizationDeclineData = Awaited> + +export const useApiAuthorizationDeclineMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => declineApiAuthorization(vars), + 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..652c19c4d5f --- /dev/null +++ b/studio/data/api-authorization/api-authorization-query.ts @@ -0,0 +1,57 @@ +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 = { + name: string + website: string + domain: string + expires_at: string + approved_at: string | null + approved_organization_slug?: 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/authorizations/${id}`, { signal }) + if (response.error) throw response.error + return response as ApiAuthorizationResponse +} + +export type ResourceData = Awaited> +export type ResourceError = { errorEventId: string; message: string } + +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/lib/auth.tsx b/studio/lib/auth.tsx index c7b962a2010..55b17f74866 100644 --- a/studio/lib/auth.tsx +++ b/studio/lib/auth.tsx @@ -1,18 +1,20 @@ import { AuthContext as AuthContextInternal, AuthProvider as AuthProviderInternal, - AuthProviderProps, gotrueClient, + useTelemetryProps, } from 'common' import { useProfileQuery } from 'data/profile/profile-query' import { useStore } from 'hooks' import { PropsWithChildren, useEffect } from 'react' import { GOTRUE_ERRORS, IS_PLATFORM } from './constants' +import Telemetry from 'lib/telemetry' export const AuthContext = AuthContextInternal export const AuthProvider = ({ children }: PropsWithChildren<{}>) => { const { ui, app } = useStore() + const telemetryProps = useTelemetryProps() // Check for unverified GitHub users after a GitHub sign in useEffect(() => { @@ -34,7 +36,7 @@ export const AuthProvider = ({ children }: PropsWithChildren<{}>) => { // Track telemetry for the current user useProfileQuery({ onSuccess(profile) { - ui.setProfile(profile) + Telemetry.sendIdentify(profile, telemetryProps) // [Joshen] Temp fix: For new users, the GET profile call also creates a default org // But because the dashboard's logged in state is using gotrue as the source of truth diff --git a/studio/lib/telemetry.ts b/studio/lib/telemetry.ts index ea5dcb38bcb..9cf951fc0c6 100644 --- a/studio/lib/telemetry.ts +++ b/studio/lib/telemetry.ts @@ -1,8 +1,9 @@ import { post } from 'lib/common/fetch' import { API_URL, IS_PLATFORM } from 'lib/constants' import { User } from 'types' +import { NextRouter } from 'next/router' -export interface GoogleAnalyticsProps { +export interface TelemetryProps { screenResolution?: string language: string } @@ -14,7 +15,8 @@ const sendEvent = ( label: string value?: string }, - gaProps?: GoogleAnalyticsProps + gaProps: TelemetryProps, + router: NextRouter ) => { if (!IS_PLATFORM) return @@ -25,6 +27,9 @@ const sendEvent = ( category: category, label: label, value: value, + page_referrer: document?.referrer, + page_title: document?.title, + page_location: router.asPath, ga: { screen_resolution: gaProps?.screenResolution, language: gaProps?.language, @@ -36,7 +41,7 @@ const sendEvent = ( * TODO: GA4 doesn't have identify method. * We may or may not need gaClientId here. Confirm later */ -const sendIdentify = (user: User, gaProps?: GoogleAnalyticsProps) => { +const sendIdentify = (user: User, gaProps?: TelemetryProps) => { if (!IS_PLATFORM) return return post(`${API_URL}/telemetry/identify`, { diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts index 8677334f21b..13383de212c 100644 --- a/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts +++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts @@ -2,7 +2,7 @@ import { createClient } from '@supabase/supabase-js' import apiWrapper from 'lib/api/apiWrapper' import { NextApiRequest, NextApiResponse } from 'next' -const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) +const supabase = createClient(process.env.SUPABASE_PUBLIC_URL!, process.env.SUPABASE_SERVICE_KEY!) export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx new file mode 100644 index 00000000000..86559bebe6e --- /dev/null +++ b/studio/pages/authorize.tsx @@ -0,0 +1,232 @@ +import { useParams } from 'common' +import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout' +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 [isSubmitting, setIsSubmitting] = useState(false) + const [selectedOrg, setSelectedOrg] = useState() + + const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() + const { data: requester, isLoading, isError, error } = useApiAuthorizationQuery({ id: auth_id }) + const isApproved = requester?.approved_at !== null + 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) + } + } + + 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 }) + ui.setNotification({ category: 'success', message: 'Declined API authorization request' }) + 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 ( + Authorization for API access

}> +
+ + Please provide a valid authorization ID in the URL + +
+
+ ) + } + + if (isError) { + return ( + Authorize API access

}> +
+ +

Please retry your authorization request from the requesting app

+ {error !== undefined &&

Error: {error?.message}

} +
+
+
+ ) + } + + 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 {requester?.name}

} + footer={ +
+
+ + +
+
+ } + > +
+ {/* API Authorization requester details */} + +
+
+
+
+

{requester?.name[0]}

+
+
+
+

+ {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 + + )} + + {/* Organization selection */} + {isLoadingOrganizations ? ( +
+ + +
+ ) : ( + + {(organizations ?? []).map((organization) => ( + + {organization.name} + + ))} + + )} +
+
+ ) +} + +APIAuthorizationPage.getLayout = (page) => {page} +export default withAuth(APIAuthorizationPage) diff --git a/studio/stores/UiStore.ts b/studio/stores/UiStore.ts index 1b91898ae31..0e7b35339f9 100644 --- a/studio/stores/UiStore.ts +++ b/studio/stores/UiStore.ts @@ -1,8 +1,7 @@ import { uuidv4 } from 'lib/helpers' import { action, makeAutoObservable } from 'mobx' -import { Project, Notification, User, Organization, ProjectBase, Permission } from 'types' +import { Project, Notification, Organization, ProjectBase, Permission } from 'types' import { IRootStore } from './RootStore' -import Telemetry, { GoogleAnalyticsProps } from 'lib/telemetry' export interface IUiStore { language: 'en-US' @@ -12,12 +11,10 @@ export interface IUiStore { selectedOrganization?: Organization notification?: Notification permissions?: Permission[] - googleAnalyticsProps?: GoogleAnalyticsProps load: () => void setProjectRef: (ref?: string) => void setOrganizationSlug: (slug?: string) => void setNotification: (notification: Notification) => string - setProfile: (value: User) => void setPermissions: (permissions?: Permission[]) => void } export default class UiStore implements IUiStore { @@ -83,14 +80,6 @@ export default class UiStore implements IUiStore { return undefined } - get googleAnalyticsProps() { - return { - screenResolution: - typeof window !== 'undefined' ? `${window.innerWidth}x${window.innerHeight}` : undefined, - language: this.language, - } - } - load() { if (typeof window === 'undefined') return } @@ -109,10 +98,6 @@ export default class UiStore implements IUiStore { return id } - setProfile(value: User) { - Telemetry.sendIdentify(value, this.googleAnalyticsProps) - } - setPermissions(permissions?: any) { this.permissions = permissions }