Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2023-05-09 11:49:00 -02:30
commit c8f0b14a25
39 files changed
+858 -106

No files matched your search

+1
View File
@@ -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
+1 -3
View File
@@ -1,7 +1,5 @@
interface DataProps {
referrer?: string
title: string
route?: string
[prop: string]: any
}
export const post = (url: string, data: DataProps, options = {}) => {
+8 -3
View File
@@ -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,
},
})
}
@@ -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
+14 -10
View File
@@ -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.
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable.ai">
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.
</Quote>
## 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.
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable.ai">
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.
</Quote>
+50 -4
View File
@@ -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 = () => {
<Badge dot>{label}</Badge>
</div>
)}
{url && <TextLink label={label ? 'Get notified' : 'Learn more'} url={url} />}
{url && (
<TextLink
label={label ? 'Get notified' : 'Learn more'}
url={url}
onClick={() => sendTelemetryEvent(name)}
/>
)}
</div>
)
})
+16 -5
View File
@@ -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 (
<div className="overflow-hidden">
@@ -40,14 +49,16 @@ const Hero = () => {
</div>
<div className="flex items-center gap-2">
<Link href="https://app.supabase.com" as="https://app.supabase.com" passHref>
<a>
<a onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_startProject'])}>
<Button size="small" className="text-white">
Start your project
</Button>
</a>
</Link>
<Link href="/docs" as="/docs" passHref>
<a>
<a
onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])}
>
<Button size="small" type="default" icon={<IconBookOpen />}>
Documentation
</Button>
+15 -13
View File
@@ -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 (
<a
href={url}
className="text-scale-1100 hover:text-scale-1200 mt-3 block cursor-pointer text-sm"
>
<div className="group flex items-center gap-1">
<span className="sr-only">{`${label} about ${url}`}</span>
<span>{label}</span>
<div className="transition-all group-hover:ml-0.5">
<IconChevronRight size={14} strokeWidth={2} />
<Link href={url} passHref>
<a
className="text-scale-1100 hover:text-scale-1200 mt-3 block cursor-pointer text-sm"
{...props}
>
<div className="group flex items-center gap-1">
<span className="sr-only">{`${label} about ${url}`}</span>
<span>{label}</span>
<div className="transition-all group-hover:ml-0.5">
<IconChevronRight size={14} strokeWidth={2} />
</div>
</div>
</div>
</a>
</a>
</Link>
)
}
+17
View File
@@ -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.",
-8
View File
@@ -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",
+3
View File
@@ -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
+1 -3
View File
@@ -1,7 +1,5 @@
interface DataProps {
referrer?: string
title: string
route?: string
[prop: string]: any
}
export const post = (url: string, data: DataProps, options = {}) => {
+45
View File
@@ -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
+53
View File
@@ -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',
},
]
+42
View File
@@ -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,
}
+9 -4
View File
@@ -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,
},
})
}
+13 -6
View File
@@ -5,42 +5,49 @@
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 15 Feb 2023 03:03:30 +0000</lastBuildDate>
<lastBuildDate>Thu, 04 May 2023 22:10:00 +0000</lastBuildDate>
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/customers/mendableai</guid>
<title>Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.</title>
<link>https://supabase.com/customers/mendableai</link>
<description>How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.</description>
<pubDate>Thu, 04 May 2023 22:10:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/happyteams</guid>
<title>HappyTeams unlocks better performance and reduces cost with Supabase.</title>
<link>https://supabase.com/customers/happyteams</link>
<description>How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.</description>
<pubDate>Wed, 15 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Wed, 15 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/xendit</guid>
<title>Xendit use Supabase and create a full solution shipped to production in less than one week.</title>
<link>https://supabase.com/customers/xendit</link>
<description>As a payment processor, Xendit are responsible for verifying that all transactions are legal.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/replenysh</guid>
<title>Replenysh uses Supabase to implement OTP in less than 24 hours.</title>
<link>https://supabase.com/customers/replenysh</link>
<description>With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/mobbin</guid>
<title>How Mobbin migrated 200,000 users from Firebase for a better authentication experience.</title>
<link>https://supabase.com/customers/mobbin</link>
<description>Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/epsilon3</guid>
<title>Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations.</title>
<link>https://supabase.com/customers/epsilon3</link>
<description>Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
</channel>
+1 -1
View File
@@ -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",
+1
View File
@@ -1,2 +1,3 @@
export * from './useBreakpoint'
export * from './useParams'
export * from './useTelemetryProps'
@@ -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',
}
}
+8
View File
@@ -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",
+20 -3
View File
@@ -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'
@@ -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',
@@ -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
@@ -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
)
}}
/>
@@ -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<CSVLink & HTMLAnchorElement & { link: HTMLAnchorElement }>(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
)
})
}
@@ -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<APIAuthorizationLayoutProps>) => {
const { isDarkMode } = useTheme()
return (
<>
<Head>
<title>Authorize API access | Supabase</title>
</Head>
<main className="flex-grow flex flex-col w-full h-full overflow-y-auto">
<div>
<div className="mx-auto px-4 sm:px-6">
<div className="max-w-xl flex justify-between items-center mx-auto py-4">
<div className="flex justify-start lg:w-0 lg:flex-1">
<div>
<span className="sr-only">Supabase</span>
<Image
src={
isDarkMode
? `${BASE_PATH}/img/supabase-dark.svg`
: `${BASE_PATH}/img/supabase-light.svg`
}
alt="Supabase Logo"
height={20}
width={105}
/>
</div>
</div>
</div>
</div>
</div>
<Divider light />
<div className="flex flex-col justify-center flex-grow mx-auto max-w-[90vw] md:max-w-xl h-full space-y-4">
{children}
</div>
</main>
</>
)
}
export default APIAuthorizationLayout
+4 -4
View File
@@ -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,
},
})
}
@@ -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<ReturnType<typeof approveApiAuthorization>>
export const useApiAuthorizationApproveMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<ApiAuthorizationApproveData, unknown, ApiAuthorizationApproveVariables>,
'mutationFn'
> = {}) => {
return useMutation<ApiAuthorizationApproveData, unknown, ApiAuthorizationApproveVariables>(
(vars) => approveApiAuthorization(vars),
options
)
}
@@ -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<ReturnType<typeof declineApiAuthorization>>
export const useApiAuthorizationDeclineMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<ApiAuthorizationDeclineData, unknown, ApiAuthorizationDeclineVariables>,
'mutationFn'
> = {}) => {
return useMutation<ApiAuthorizationDeclineData, unknown, ApiAuthorizationDeclineVariables>(
(vars) => declineApiAuthorization(vars),
options
)
}
@@ -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<ReturnType<typeof getApiAuthorizationDetails>>
export type ResourceError = { errorEventId: string; message: string }
export const useApiAuthorizationQuery = <TData = ResourceData>(
{ id }: ApiAuthorizationVariables,
{ enabled = true, ...options }: UseQueryOptions<ResourceData, ResourceError, TData> = {}
) =>
useQuery<ResourceData, ResourceError, TData>(
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])
}
+3
View File
@@ -0,0 +1,3 @@
export const resourceKeys = {
resource: (id: string | undefined) => ['api-authorization', id] as const,
}
+1
View File
@@ -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) =>
@@ -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<ReturnType<typeof getOrganizations>>
export type OrganizationsError = unknown
export const useOrganizationsQuery = <TData = OrganizationsData>({
enabled = true,
...options
}: UseQueryOptions<OrganizationsData, OrganizationsError, TData> = {}) =>
useQuery<OrganizationsData, OrganizationsError, TData>(
organizationKeys.list(),
({ signal }) => getOrganizations(signal),
{ enabled: enabled, ...options }
)
export const useOrganizationsPrefetch = () => {
const client = useQueryClient()
return useCallback(
() => client.prefetchQuery(organizationKeys.list(), ({ signal }) => getOrganizations(signal)),
[]
)
}
+4 -2
View File
@@ -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
+8 -3
View File
@@ -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`, {
@@ -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)
+232
View File
@@ -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<string>()
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 (
<FormPanel header={<p>Authorize API access</p>}>
<div className="w-[500px] px-8 py-6 space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
</FormPanel>
)
}
if (auth_id === undefined) {
return (
<FormPanel header={<p>Authorization for API access</p>}>
<div className="w-[500px] px-8 py-6">
<Alert withIcon variant="warning" title="Missing authorization ID">
Please provide a valid authorization ID in the URL
</Alert>
</div>
</FormPanel>
)
}
if (isError) {
return (
<FormPanel header={<p>Authorize API access</p>}>
<div className="w-[500px] px-8 py-6">
<Alert
withIcon
variant="warning"
title="Failed to fetch details for API authorization request"
>
<p>Please retry your authorization request from the requesting app</p>
{error !== undefined && <p className="mt-2">Error: {error?.message}</p>}
</Alert>
</div>
</FormPanel>
)
}
if (isApproved) {
const approvedOrganization = organizations?.find(
(org) => org.slug === requester.approved_organization_slug
)
return (
<FormPanel header={<p>Authorize API access for {requester?.name}</p>}>
<div className="w-full md:w-[500px] px-8 py-6 space-y-8">
<Alert withIcon variant="success" title="This authorization request has been approved">
<p>
{requester.name} has read and write access to the organization "
{approvedOrganization?.name ?? 'Unknown'}" and all of its projects
</p>
<p className="mt-2">
Approved on: {dayjs(requester.approved_at).format('DD MMM YYYY HH:mm:ss (ZZ)')}
</p>
</Alert>
</div>
</FormPanel>
)
}
return (
<FormPanel
header={<p>Authorize API access for {requester?.name}</p>}
footer={
<div className="flex items-center justify-end py-4 px-8">
<div className="flex items-center space-x-2">
<Button type="default" disabled={isSubmitting || isExpired} onClick={onDeclineRequest}>
Decline
</Button>
<Button
loading={isSubmitting}
disabled={isSubmitting || isExpired}
onClick={onApproveRequest}
>
Authorize {requester?.name}
</Button>
</div>
</div>
}
>
<div className="w-full md:w-[500px] px-8 py-6 space-y-8">
{/* API Authorization requester details */}
<div className="flex space-x-4">
<div>
<div className="rounded-md border border-scale-600 p-2.5 flex items-center">
<div
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}')` }}
>
<p className="text-scale-1000 text-lg">{requester?.name[0]}</p>
</div>
</div>
</div>
<p className="text-sm text-scale-1100">
{requester?.name} ({requester?.domain}) is requesting API access to an organization. The
application will be able to{' '}
<span className="text-amber-1200">
read and write the organization's settings and all of its projects.
</span>
</p>
</div>
{/* Expiry warning */}
{isExpired && (
<Alert withIcon variant="warning" title="This authorization request is expired">
Please retry your authorization request from the requesting app
</Alert>
)}
{/* Organization selection */}
{isLoadingOrganizations ? (
<div className="py-4 space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
</div>
) : (
<Listbox
label="Select an organization to grant API access to"
value={selectedOrg}
disabled={isExpired}
onChange={setSelectedOrg}
>
{(organizations ?? []).map((organization) => (
<Listbox.Option
key={organization.id}
label={organization.name}
value={organization.slug}
>
{organization.name}
</Listbox.Option>
))}
</Listbox>
)}
</div>
</FormPanel>
)
}
APIAuthorizationPage.getLayout = (page) => <APIAuthorizationLayout>{page}</APIAuthorizationLayout>
export default withAuth(APIAuthorizationPage)
+1 -16
View File
@@ -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
}