diff --git a/apps/docs/pages/guides/auth.mdx b/apps/docs/pages/guides/auth.mdx index f12e0178821..3aca4c66333 100644 --- a/apps/docs/pages/guides/auth.mdx +++ b/apps/docs/pages/guides/auth.mdx @@ -91,20 +91,20 @@ const getURL = () => { let url = process?.env?.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env. process?.env?.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel. - 'http://localhost:3000/'; + 'http://localhost:3000/' // Make sure to include `https://` when not localhost. - url = url.includes('http') ? url : `https://${url}`; - // Make sure to including trailing `/`. - url = url.charAt(url.length - 1) === '/' ? url : `${url}/`; - return url; -}; + url = url.includes('http') ? url : `https://${url}` + // Make sure to include a trailing `/`. + url = url.charAt(url.length - 1) === '/' ? url : `${url}/` + return url +} const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'github' + provider: 'github', options: { - redirectTo: getURL() - } -} + redirectTo: getURL(), + }, +}) ``` #### Mobile deep linking URIs diff --git a/apps/docs/pages/guides/auth/social-login/auth-apple.mdx b/apps/docs/pages/guides/auth/social-login/auth-apple.mdx index 8f72a47a5d2..2285fbd35ad 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-apple.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-apple.mdx @@ -4,95 +4,53 @@ import AppleSecretGenerator from '~/components/AppleSecretGenerator' export const meta = { id: 'auth-apple', title: 'Login with Apple', - description: 'Add Apple OAuth to your Supabase project', + description: 'Use Sign in with Apple with Supabase', } -To enable Apple Auth for your project, you need to set up an Apple OAuth application and add the application credentials to your Supabase Dashboard. +Supabase Auth supports using [Sign in with Apple](https://developer.apple.com/sign-in-with-apple/) on the web and in native apps for iOS, macOS, watchOS or tvOS. ## Overview -Apple OAuth consists of six broad steps: +To support Sign in with Apple, you need to configure the [Apple provider in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) for your project. -- Obtaining an `App Id` with “Sign In with Apple” capabilities. -- Obtaining a `Services Id` - this will serve as the `client_id`. -- Obtaining a `secret key` that will be used to get our `client_secret`. -- Generating the `client_secret` using the `secret key`. -- Add your `client id` and `client secret` keys to your [Supabase Project](https://app.supabase.com). -- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js). +There are three general ways to use Sign in with Apple, depending on the application you're trying to build: -## Access your Apple Developer account +- Sign in on the web or in web-based apps + - Using an OAuth flow initiated by Supabase Auth using the [Sign in with Apple REST API](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api). + - Using [Sign in with Apple JS](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js) directly in the browser, usually suitable for websites. +- Sign in natively inside iOS, macOS, watchOS or tvOS apps using [Apple's Authentication Services](https://developer.apple.com/documentation/authenticationservices) -- Go to [developer.apple.com](https://developer.apple.com). -- Click on `Account` at the top right to log in. +In some cases you're able to use the OAuth flow within web-based native apps such as with [React Native](https://reactnative.dev), [Expo](https://expo.dev) or other similar frameworks. It is best practice to use native Sign in with Apple capabilities on those platforms instead. -![Apple Developer Portal.](/docs/img/guides/auth-apple/apple-developer-portal.png) +Before you can use Sign in with Apple, you need to obtain an [Apple Developer](https://developer.apple.com) account. -## Obtain an App ID +## Using the OAuth flow for web -- Go to `Certificates, Identifiers & Profiles`. -- Click on `Identifiers` at the left. -- Click on the `+` sign in the upper left next to `Identifiers`. -- Select `App IDs` and click `Continue`. -- Select type `App` and click `Continue`. -- Fill out your app information: - - App description. - - Bundle ID (Apple recommends reverse-domain name style, so if your domain is acme.com and your app is called roadrunner, use: "com.acme.roadrunner"). - - Scroll down and check `Sign In With Apple`. - - Click `Continue` at the top right. - - Click `Register` at the top right. +Sign in with Apple's OAuth flow is designed for web or browser based sign in methods. It can be used on web-based apps as well as websites, though some users can benefit by using Sign in with Apple JS directly. -## Obtain a Services ID +Behind the scenes, Supabase Auth uses the [REST APIs](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) provided by Apple. -This will serve as the `client_id` when you make API calls to authenticate the user. +To initiate sign in, you can use the `signInWithOAuth()` method from the Supabase JavaScript library: -- Go to `Certificates, Identifiers & Profiles`. -- Click on `Identifiers` at the left. -- Click on the `+` sign in the upper left next to `Identifiers`. -- Select `Services IDs` and click `Continue`. -- Fill out your information: - - App description. - - Bundle ID (you can't use the same Bundle ID from the previous step, but you can just add something to the beginning, such as "app." to make it app.com.acme.roadrunner"). - - SAVE THIS ID -- this ID will become your `client_id` later. - - Click `Continue` at the top right. - - Click `Register` at the top right. +```ts +supabase.auth.signInWithOAuth({ + provider: 'apple', +}) +``` -## Find your callback URL +This call takes the user to Apple's consent screen. Once the flow ends, the user's profile information is exchanged and validated with Supabase Auth before it redirects back to your web application with an access and refresh token representing the user's session. - +### Configuration -## Configure your Services ID +You will require the following information: -- Under `Identifiers`, click on your newly-created Services ID. -- Check the box next to `Sign In With Apple` to enable it. -- Click `Configure` to the right. -- Make sure your newly created Bundle ID is selected under `Primary App ID` -- Add your domain to the `Domains and Subdomains` box (do not add `https://`, just add the domain). -- In the `Return URLs` box, type the callback URL of your app which you found in the previous step and click `Next` at the bottom right. -- Click `Done` at the bottom. -- Click `Continue` at the top right. -- Click `Save` at the top right. - -## Download your secret key - -Now you'll need to download a `secret key` file from Apple that will be used to generate your `client_secret`. - -- Go to `Certificates, Identifiers & Profiles`. -- Click on `Keys` at the left. -- Click on the `+` sign in the upper left next to `Keys`. -- Enter a `Key Name`. -- Check `Sign In with Apple`. -- Click `Configure` to the right. -- Select your newly-created Services ID from the dropdown selector. -- Click `Save` at the top right. -- Click `Continue` at the top right. -- Click `Register` at the top right. -- Click `Download` at the top right. -- Save the downloaded file -- this contains your "secret key" that will be used to generate your `client_secret`. -- Click `Done` at the top right. - -## Generate a client secret - -You need to configure a client secret when using Sign in with Apple for Web. This is a specially crafted [JWT signed with a secret key downloaded from Apple's Developer Center](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens). +1. Your Apple Developer account's **Team ID**, which is an alphanumeric string of 10 characters that uniquely identifies the developer of the app. It's often easily accessible in the upper right-side menu on the Apple Developer Console. +2. Register email sources for _Sign in with Apple for Email Communication_ which can be found in the [Services](https://developer.apple.com/account/resources/services/list) section of the Apple Developer Console. +3. An **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple once you create an App ID in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) +4. A **Services ID** which uniquely identifies the web services provided by the app you registered in the previous step. You can create a new Services ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/serviceId) section in the Apple Developer Console (use the filter menu in the upper right side to see all Services IDs). These usually are a reverse domain name string, for example `com.example.app.web`. +5. Configure Website URLs for the newly created **Services ID**. The web domain you should use is the domain your Supabase project is hosted on. This is usually `.supabase.co` while the redirect URL is `https://.supabase.co/auth/v1/callback`. +6. Create a signing **Key** in the [Keys](https://developer.apple.com/account/resources/authkeys/list) section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Supabase project's Auth configuration. Make sure you safely store the `AuthKey_XXXXXXXXXX.p8` file. If you ever loose access to it, or make it public accidentally please revoke it from the Apple Developer Console and create a new one immediately. You will have to generate a new secret key using this file every 6 months, so make sure you schedule a recurring meeting in your calendar! +7. Finally, add the information you configured above to the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers). Use this tool to generate a new Apple client secret. No keys leave your browser! @@ -100,34 +58,113 @@ You need to configure a client secret when using Sign in with Apple for Web. Thi -## Add your OAuth credentials to Supabase +## Using native sign in - +Unlike the OAuth flow which requires the use of a web browser, the native Sign in with Apple flow on iOS, macOS, watchOS or tvOS uses the [operating system's built-in functionalities](https://developer.apple.com/documentation/authenticationservices) to prompt the user for consent. -## Add login code to your client app +When the user provides consent, Apple issues an identity token (commonly abbreviated as ID token) that is then sent to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth. -When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `apple` as the `provider`: +If you are building an iOS or macOS app using Flutter, you can use [apple_sign_in](https://pub.dev/packages/sign_in_with_apple) package to quickly get the user's consent and sign them in to your Supabase project: -```js -async function signInWithApple() { - const { data, error } = await supabase.auth.signInWithOAuth({ +```dart +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; +import 'package:crypto/crypto.dart'; + +Future signInWithApple() { + + // Generate a random string + final rawNonce = _generateRandomString(); + final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString(); + + final credential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + nonce: hashedNonce, + ); + + final idToken = credential.identityToken; + if (idToken == null) { + throw 'Could not find ID Token from generated credential.'; + } + + return signInWithIdToken( + provider: Provider.apple, + idToken: idToken, + nonce: rawNonce, + ); +} +``` + +In the Supabase JavaScript library, which you can use with web-based native frameworks like React Native or Expo, you can invoke this functionality like so: + +```ts +await supabase.auth.signInWithIdToken({ + provider: 'apple', + token: '', +}) +``` + +Please take a look at these open-source projects which may help you obtain an ID token directly from the OS: + +- [invertase/react-native-apple-authentication](https://github.com/invertase/react-native-apple-authentication) +- [Expo AppleAuthentication](https://docs.expo.dev/versions/latest/sdk/apple-authentication/) + +For apps written in Swift, please consult the [community maintained library](/docs/reference/swift/introduction). + +### Configuration + +Native sign in requires less configuration steps than OAuth flow. You will need to perform these steps: + +1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) +2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Authorized Client IDs_. + +Note that if you're building a native app only, you do not need to setup the OAuth flow. + +## Using Sign in with Apple JS + +[Sign in with Apple JS](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js) is an official Apple framework for authenticating Apple users on websites. Although it can be used in web-based apps, those use cases will benefit more with the OAuth flow described above. We recommend using this method on classic websites only. + +You can use the `signInWithIdToken()` method from the Supabase JavaScript library on the website to obtain an access and refresh token once the user has given consent using Sign in with Apple JS: + +```ts +function signIn() { + const data = await AppleID.auth.signIn() + + await supabase.auth.signInWithIdToken({ provider: 'apple', + token: data.id_token, + nonce: '', }) } ``` -When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: +Alternatively, you can use the `AppleIDSignInOnSuccess` event with the `usePopup` option: -```js -async function signout() { - const { error } = await supabase.auth.signOut() -} +```ts +// Listen for authorization success. +document.addEventListener('AppleIDSignInOnSuccess', async (event) => { + await supabase.auth.signInWithIdToken({ + provider: 'apple', + token: event.data.id_token, + nonce: '', + }) +}) ``` -## Resources +Please make sure you request for the scope `name email` when initializing the library. -- [Apple Developer Account](https://developer.apple.com). -- Thanks to [Janak Amarasena](https://medium.com/@janakda) who did all the heavy lifting in [How to configure Sign In with Apple](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003). +### Configuration + +To use Sign in with Apple JS you need to configure these options: + +1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) +2. Obtain a **Services ID** attached to the App ID that uniquely identifies the website. Use this value as the client ID when initializing Sign in with Apple JS. You can create a new Services ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/serviceId) section in the Apple Developer Console (use the filter menu in the upper right side to see all Services IDs). These usually are a reverse domain name string, for example `com.example.app.website`. +3. Configure Website URLs for the newly created **Services ID**. The web domain you should use is the domain your website is hosted on. The redirect URL must also point to a page on your website that will receive the callback from Apple. +4. Register the Services ID you created to your project's [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Authorized Client IDs_. + +Note that if you're using Sign in with Apple JS you do not need to configure the OAuth settings. export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/auth/social-login/auth-google.mdx b/apps/docs/pages/guides/auth/social-login/auth-google.mdx index ef5d4ccffb0..164306f722b 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-google.mdx @@ -3,120 +3,288 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { id: 'auth-google', title: 'Login with Google', - description: 'Add Google OAuth to your Supabase project', + description: 'Use Sign in with Google on the web, in native apps or with Chrome extensions', } -To enable Google Auth for your project, you need to set up a Google OAuth application and add the application credentials to your Supabase Dashboard. +Supabase Auth supports Sign in with Google on the web, native Android applications and Chrome extensions. ## Overview -Setting up Google logins for your application consists of 3 parts: +To support Sign in with Google you need to configure the [Google provider in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) for your project. -- Create and configure a Google Project on the [Google Cloud Platform Console](https://console.cloud.google.com/home/dashboard) -- Add your Google OAuth keys to your [Supabase Project](https://app.supabase.com) -- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) +There are three general ways to use Sign in with Google, depending on the application you're building: -## Access your Google Cloud Platform account +- Sign in on the web or in web-based apps + - Using an OAuth flow initiated by Supabase Auth using [Google Identity Authorization with OAuth 2.0 for Web Server Applications](https://developers.google.com/identity/protocols/oauth2/web-server). + - Using a [personalized sign-in button](https://developers.google.com/identity/gsi/web/guides/personalized-button) or [One Tap](https://developers.google.com/identity/gsi/web/guides/display-google-one-tap) and [automatic sign-in](https://developers.google.com/identity/gsi/web/guides/automatic-sign-in-sign-out) for users already logged in to their Google account. +- Sign in in native Android apps and Chrome extensions -- Go to [cloud.google.com](https://cloud.google.com). -- Click on `Sign in` at the top right to log in. +In some cases you're able to use the OAuth flow within web-based native apps such as with React Native, Expo or other similar frameworks. It is best practice to use native Sign in with Google capabilities whenever possible. -![Google Developer Portal.](/docs/img/guides/auth-google/google-portal.png) +Before you can use Sign in with Google, you need to obtain a [Google Cloud Platform](https://console.cloud.google.com/home/dashboard) account and have a project ready or create a new one. -## Create a Google Cloud Platform Project +## Using the OAuth flow for the web -- Click on `Select a Project` at the top left. - - (Or, if a project is currently selected, click on the current project name at the top left.) -- Click `New Project` at the top right. -- Fill in your app information, then click `Create`. - - (This can take a few minutes.) -- This should bring you to the dashboard for your new project. +Sign in with Google's OAuth flow is designed for web or browser based sign in methods. It can be used in web-based apps as well as in websites, though sometimes it is worthwhile considering using One Tap login directly. -## Create the OAuth Keys for your project +Behind the scenes, Supabase Auth uses the [Google OAuth 2.0 APIs](https://developers.google.com/identity/openid-connect/openid-connect), which are OpenID Connect certified, to perform the authentication. -From your project's dashboard screen: +To initiate sign in, you can use the `signInWithOAuth()` method from the Supabase JavaScript library: -- In the search bar at the top labeled `Search products and resources` type `OAuth`. -- Click on `OAuth consent screen` from the list of results. -- On the `OAuth consent screen` page select `External`. -- Click `Create`. - -## Edit your app information - -- On the `Edit app registration` page fill out your app information. -- Click `Save and continue` at the bottom. - -## Find your callback URL - - - -## Create your Google credentials - -- Click `Credentials` at the left to go to the `Credentials` page on the Google Cloud Platform console. -- Click `Create Credentials` near the top then select `OAuth client ID` -- On the `Create OAuth client ID` page, select your application type. If you're not sure, choose `Web application`. -- Fill in your app name. -- At the bottom, under `Authorized redirect URIs` click `Add URI`. -- Enter your callback URI under `Authorized redirect URIs` at the bottom. -- Enter your callback URI in the `Valid OAuth Redirect URIs` box. -- Click `Save Changes` at the bottom right. -- Click `Create`. - -Copy your new OAuth credentials - -- A box will appear called `OAuth client created`. -- Copy and save the values under `Your Client ID` and `Your Client Secret`. - -## Enter your Google credentials into your Supabase Project - - - -## Add login code to your client app - -When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `google` as the `provider`: - -```js -async function signInWithGoogle() { - const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'google', - }) -} +```ts +supabase.auth.signInWithOAuth({ + provider: 'google', +}) ``` -You can view the full list of query parameters and their descriptions [here](https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient). +This call takes the user to Google's consent screen. When the flow ends, the user's profile information is exchanegd and validated with Supabase Auth before it redirects back to your web application with an access and refresh token representing the user's session. -When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: +You can additionally extract the `provider_token` from the session (on initial login only) which is the OAuth 2.0 access token issued by Google that grants your application access to the Google services for the authenticated users. Please store this token in local storage, cookies or in your database or server. -```js -async function signout() { - const { error } = await supabase.auth.signOut() -} -``` +Google does not send out a refresh token by default, so you will need to pass parameters like these to `signInWithOAuth()` in order to extract the `provider_refresh_token`: -## Obtain the provider refresh token - -Google OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to add additional query parameters: - -```js -async function signInWithGoogle() { - const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'google', - options: { - queryParams: { - access_type: 'offline', - prompt: 'consent', - hd: 'domain.com', // google will also allow OAuth logins to be restricted to a specified domain using the 'hd' parameter - }, +```ts +const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + queryParams: { + access_type: 'offline', + prompt: 'consent', }, + }, +}) +``` + +### Configuration + +To use the OAuth 2.0 flow, you will require the following information: + +1. Obtain OAuth credentials for your Google Cloud project in the [Credentials](https://console.developers.google.com/apis/credentials) page of the console. When creating a new credential, choose _Web application_. In _Authorized redirect URIs_ enter `https://.supabase.co/auth/v1/callback`. This URL will be seen by your users, and you can customize it by configuring [custom domains](/docs/guides/platform/custom-domains). +2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. Within _Authorized domains_ make sure you add your Supabase project's domain `.supabase.co`. Configure the non-sensitive scopes by making sure the following ones are selected: `.../auth/userinfo.email`, `.../auth/userinfo.profile`, `openid`. If you're selecting other sensitive scopes, your app may require additional verification. In those cases, it's best to use [custom domains](/docs/guides/platform/custom-domains). +3. Finally, add the client ID and secret from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers). + +## Using native sign in + +Unlike the OAuth flow which requires the use of a web browser, the native Sign in with Google flow on Android uses the [operating system's built-in functionalities](https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/package-summary) to prompt the user for consent. Note that native sign-in has been rebranded as _One Tap sign-in on Android_ by Google, which you should not confuse with _One Tap sign in for web_, as mentioned below. + +When the user provides consent, Google issues an identity token (commonly abbreviated as ID token) that is then sent to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth. + +If you are building a Flutter app, you can use [flutter_appauth](https://pub.dev/packages/flutter_appauth) package to sign a user into your Supabase project: + +````dart +import 'package:crypto/crypto.dart'; +import 'package:flutter_appauth/flutter_appauth.dart'; + +// Generate a random string +final rawNonce = generateRandomString(); +final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString(); + +// Your registered Google client ID here. +// This will be different for iOS and Android +const clientId = 'YOUR_GOODLE_CLIENT_ID'; + +// bundle ID for iOS, package name for Android here +final packageName = 'io.supabase.example'; + +/// fixed for google login +final redirectUrl = '$packageName:/google_auth'; + +/// fixed for google login +const discoveryUrl = + 'https://accounts.google.com/.well-known/openid-configuration'; + +final appAuth = FlutterAppAuth(); + +// authorize the user by opening the concent page +final result = await appAuth.authorize( + AuthorizationRequest( + clientId, + redirectUrl, + discoveryUrl: discoveryUrl, + nonce: hashedNonce, + scopes: [ + 'openid', + 'email', + ], + ), +); + +if (result == null) { + throw AuthException( + 'Could not find AuthorizationResponse after authorizing'); +} + +// Request the access and id token to google +final tokenResponse = await appAuth.token( + TokenRequest( + clientId, + redirectUrl, + authorizationCode: result.authorizationCode, + discoveryUrl: discoveryUrl, + codeVerifier: result.codeVerifier, + nonce: result.nonce, + scopes: [ + 'openid', + 'email', + ], + ), +); + +final idToken = tokenResponse?.idToken; + +if (idToken == null) { + throw AuthException('Could not find idToken from the token response'); +} + +await signInWithIdToken( + provider: Provider.google, + idToken: idToken, + accessToken: tokenResponse?.accessToken, + nonce: rawNonce, +);``` + +In the Supabase JavaScript library, which you can use with web-based native frameworks like React Native or Expo, you can invoke this functionality like so: + +```ts +await supabase.auth.signInWithIdToken({ + provider: 'google', + token: '', + access_token: '' +}) +```` + +Please take a look at these open-source projects which may help you obtain an ID token directly from the OS: + +- [react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin). + +For apps written in Kotlin, please consult the [community maintained library](/docs/reference/kotlin/introduction). + +### Configuration + +1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Android_ or _iOS_ depending on the mobile operating system your app is built for. + +- For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. +- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore. + +2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. +3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. + +Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in. + +## Using native sign in for Chrome extensions + +Similar to the native sign in for Android, you can use the Chrome browser's [identity APIs](https://developer.chrome.com/docs/extensions/reference/identity/) to launch an authentication flow. + +First, you need to configure your `manifest.json` file like so: + +```json +{ + "permissions": ["identity"], + "oauth2": { + "client_id": "", + "scopes": ["openid", "email", "profile"] + } +} +``` + +Then you should call the [`chrome.identity.launchWebAuthFlow()`](https://developer.chrome.com/docs/extensions/reference/identity/#method-launchWebAuthFlow) function to trigger the sign in flow. On success, call the `supabase.auth.signInWithIdToken()` function to complete sign in with your Supabase project. + +```ts +const manifest = chrome.runtime.getManifest() + +const url = new URL('https://accounts.google.com/o/oauth2/auth') + +url.searchParams.set('client_id', manifest.oauth2.client_id) +url.searchParams.set('response_type', 'id_token') +url.searchParams.set('access_type', 'offline') +url.searchParams.set('redirect_uri', `https://${chrome.runtime.id}.chromiumapp.org`) +url.searchParams.set('scope', manifest.oauth2.scopes.join(' ')) + +chrome.identity.launchWebAuthFlow( + { + url: url.href, + interactive: true, + }, + async (redirectedTo) => { + if (chrome.runtime.lastError) { + // auth was not successful + } else { + // auth was successful, extract the ID token from the redirectedTo URL + const url = new URL(redirectedTo) + const params = new URLSearchParams(url.hash) + + const { data, error } = await supabase.auth.signInWithIdToken({ + provider: 'google', + token: params.get('id_token'), + }) + } + } +) +``` + +### Configuration + +You will need to configure a client ID for your Chrome extension: + +1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Chrome extension_ for the application type. For _Item ID_ provide the unique ID of your Chrome extension. You can get this by calling `chrome.runtime.id` within the extension, or from the Web Store URL of the extension. For example, the [Google Translate extension](https://chrome.google.com/webstore/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb) has the Web Store URL `https://chrome.google.com/webstore/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb` and the last part `aapbdbdomjkkjkaonfhkkikfgjllcleb` is its unique ID. +2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. +3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. + +Note that you do not have to configure the OAuth flow in the Supabase Dashboard to sign in with Google inside Chrome extensions. + +## Using personalized sign-in buttons, One Tap or automatic sign-in + +Most web apps and websites can utilize Google's [personalized sign-in buttons](https://developers.google.com/identity/gsi/web/guides/personalized-button), [One Tap](https://developers.google.com/identity/gsi/web/guides/features) or [automatic sign-in](https://developers.google.com/identity/gsi/web/guides/automatic-sign-in-sign-out) for the best user experience. + +Under the hood, these sign in methods end with an identity token being issued by [Sign in with Google for Web](https://developers.google.com/identity/gsi/web/guides/overview). You can then use the `supabase.auth.signInWithIdToken()` method to immediately issue an access and refresh tokens for the user, without needing to build any additional UIs or flows. + +To get started, you can use the [HTML Code Generator](https://developers.google.com/identity/gsi/web/tools/configurator) to customize the look, feel, features and behavior of the Sign in with Google button. Make sure you pick the _Swap to JavaScript callback_ option and name the function that will receive a [`CredentialResponse`](https://developers.google.com/identity/gsi/web/reference/js-reference#CredentialResponse) when sign in completes. + +For example, this HTML code shows a typical Sign in with Google button: + +```html +
+ + +``` + +When the user signs in, the `handleSignInWithGoogle` function will be called: + +```ts +async function handleSignInWithGoogle(response) { + const { data, error } = await supabase.auth.signInWithIdToken({ + token: response.credential, + nonce: 'NONCE', // must be the same one as provided in data-nonce (if any) }) } ``` -## Resources +Use of nonce is recommended, though optional. Make sure each nonce is generated randomly and available both in the `data-nonce` attribute as well as in the `handleSignInWithGoogle` callback function; otherwise the ID token will not be accepted. -- [Supabase Account - Free Plan OK](https://supabase.com) -- [Supabase JS Client](https://github.com/supabase/supabase-js) -- [Google Cloud Platform Console](https://console.cloud.google.com/home/dashboard) +### Configuration + +1. Obtain OAuth credentials for your Google Cloud project in the [Credentials](https://console.developers.google.com/apis/credentials) page of the console. When creating a new credential, choose _Web application_. As you're using the Google sign in button, you should configure the _Authorized JavaScript origins_ and _Authorized redirect URIs_ to the website where the buttons appear. You should not use your Supabase project domain name. For this use case, the client secret provided is not needed and can be ignored. +2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. +3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Authorized Client IDs_. + +Note that you do not have to configure the OAuth flow client ID and secret in the Supabae Dashboard when using this approach! export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/database/connecting-to-postgres.mdx b/apps/docs/pages/guides/database/connecting-to-postgres.mdx index 45db57cd344..142f5daf35f 100644 --- a/apps/docs/pages/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/pages/guides/database/connecting-to-postgres.mdx @@ -11,7 +11,7 @@ Supabase provides several options for programmatically connecting to your Postgr 1. Direct connections using Postgres' standard connection system 2. Connection pooling using PgBouncer -3. Programmatic access uing the [Serverless APIs](/docs/guides/api) +3. Programmatic access using the [Serverless APIs](/docs/guides/api) ## Serverless APIs diff --git a/apps/docs/pages/guides/database/json.mdx b/apps/docs/pages/guides/database/json.mdx index 46e60985db4..5703522101e 100644 --- a/apps/docs/pages/guides/database/json.mdx +++ b/apps/docs/pages/guides/database/json.mdx @@ -244,7 +244,7 @@ create table customers ( ); alter table customers -add constriant check_metadata check ( +add constraint check_metadata check ( json_matches_schema( '{ "type": "object", diff --git a/apps/docs/pages/guides/functions/secrets.mdx b/apps/docs/pages/guides/functions/secrets.mdx index ffbe7920042..9f0b8be8506 100644 --- a/apps/docs/pages/guides/functions/secrets.mdx +++ b/apps/docs/pages/guides/functions/secrets.mdx @@ -12,6 +12,15 @@ It's common that you will need to use sensitive information or environment-speci Deno.env.get(MY_SECRET_NAME) ``` +### Local Development + +When developing functions locally, you be able to load environment variables two ways: + +1. Through a default `.env` file placed at `supabase/functions/.env`, which will get loaded on `supabase start` +2. Through the `--env-file` option for `supabase functions serve`, for example: `supabase functions serve --env-file ./path/to/.env-file` + +To perform a one-time setup of your local development secrets, use the first option to create the `.env` file that will apply to all functions. + ### Default secrets By default, Edge Functions have access to these secrets: diff --git a/apps/docs/pages/guides/realtime/postgres-changes.mdx b/apps/docs/pages/guides/realtime/postgres-changes.mdx index 59f2b65c22f..8cff43e6e0e 100644 --- a/apps/docs/pages/guides/realtime/postgres-changes.mdx +++ b/apps/docs/pages/guides/realtime/postgres-changes.mdx @@ -331,34 +331,37 @@ const channel = supabase ## Custom Tokens You may choose to sign your own tokens to customize claims that can be checked in your RLS policies. -In order for this to work you must pass `apikey` in both Realtime's `headers` and `params` when creating the client. -The `apikey` in `params` must be either the `anon` or `service_role` token that Supabase provides for every project. -You can find these tokens under [Project API keys](https://app.supabase.com/project/_/settings/api) in your project's dashboard. -This will authenticate your request in the API gateway. +Your project JWT secret is found with your [Project API keys](https://app.supabase.com/project/_/settings/api) in your dashboard. Do not expose the `service_role` token on the client because the role is authorized to bypass row-level security. -The `apikey` in `headers` can be your custom token signed with the JWT secret of your Supabase project. -This is forwarded to the Realtime server and it will verify your custom token and use its claims to authorize database changes -when RLS is enabled. +To use your own JWT with Realtime make sure to set the token after instantiating the Supabase client and before connecting to a Channel. ```js const { createClient } = require('@supabase/supabase-js') -const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY, { - realtime: { - headers: { - apikey: `Bearer ${your_custom_token}`, +const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY, {}) + +// Set your custom JWT here +supabase.realtime.setAuth('your-custom-jwt') + +const channel = supabase + .channel('db-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'messages', + filter: 'body=eq.bye', }, - params: { - apikey: process.env.SUPABASE_KEY, - }, - }, -}) + (payload) => console.log(payload) + ) + .subscribe() ``` ### Refreshed Tokens diff --git a/apps/www/_blog/2020-12-02-case-study-monitoro.mdx b/apps/www/_blog/2020-12-02-case-study-monitoro.mdx index 0c628e8c394..df53fac79d7 100644 --- a/apps/www/_blog/2020-12-02-case-study-monitoro.mdx +++ b/apps/www/_blog/2020-12-02-case-study-monitoro.mdx @@ -8,7 +8,6 @@ author_image_url: https://github.com/roryw10.png authorURL: https://github.com/roryw10 image: /images/blog/supabase-monitoro.png tags: - - case-study - no-code date: '12-02-2020' video: https://www.youtube.com/v/8A6_pg41M2s diff --git a/apps/www/_blog/2020-12-02-case-study-tayfa.mdx b/apps/www/_blog/2020-12-02-case-study-tayfa.mdx index a4d03ff3e5e..c494482fc63 100644 --- a/apps/www/_blog/2020-12-02-case-study-tayfa.mdx +++ b/apps/www/_blog/2020-12-02-case-study-tayfa.mdx @@ -8,7 +8,6 @@ author_image_url: https://github.com/roryw10.png authorURL: https://github.com/roryw10 image: /images/blog/supabase-tayfa.png tags: - - case-study - no-code date: '12-02-2020' --- diff --git a/apps/www/_blog/2020-12-02-case-study-xendit.mdx b/apps/www/_blog/2020-12-02-case-study-xendit.mdx index 80d8822fb17..5d61a0c70d4 100644 --- a/apps/www/_blog/2020-12-02-case-study-xendit.mdx +++ b/apps/www/_blog/2020-12-02-case-study-xendit.mdx @@ -9,7 +9,6 @@ authorURL: https://github.com/roryw10 image: supabase-xendit.png thumb: supabase-xendit-thumb.jpg tags: - - case-study - fintech date: '12-02-2020' --- diff --git a/apps/www/_blog/2021-02-09-case-study-roboflow.mdx b/apps/www/_blog/2021-02-09-case-study-roboflow.mdx index 97888890d83..5b33ea3b7d8 100644 --- a/apps/www/_blog/2021-02-09-case-study-roboflow.mdx +++ b/apps/www/_blog/2021-02-09-case-study-roboflow.mdx @@ -9,7 +9,6 @@ authorURL: https://github.com/roryw10 image: roboflow-og.png thumb: roboflow-website.png tags: - - case-study - AI date: '02-09-2021' --- diff --git a/apps/www/_blog/2021-03-08-toad-a-link-shortener-with-simple-apis-for-low-coders.mdx b/apps/www/_blog/2021-03-08-toad-a-link-shortener-with-simple-apis-for-low-coders.mdx index a87778aaeb0..c1ddf4af7e2 100644 --- a/apps/www/_blog/2021-03-08-toad-a-link-shortener-with-simple-apis-for-low-coders.mdx +++ b/apps/www/_blog/2021-03-08-toad-a-link-shortener-with-simple-apis-for-low-coders.mdx @@ -10,7 +10,6 @@ image: toadli-og.jpg thumb: toadli-website.jpg tags: - supabase - - case-study date: '03-08-2021' --- diff --git a/apps/www/_blog/2021-07-26-epsilon3-self-hosting.mdx b/apps/www/_blog/2021-07-26-epsilon3-self-hosting.mdx index f5005583c8f..905bbccdeb2 100644 --- a/apps/www/_blog/2021-07-26-epsilon3-self-hosting.mdx +++ b/apps/www/_blog/2021-07-26-epsilon3-self-hosting.mdx @@ -8,7 +8,7 @@ authorURL: https://github.com/roryw10 image: epsilon3/og-epsilon3.jpg thumb: epsilon3/cover-epsilon3.jpg tags: - - case-study + - supabase date: '2021-07-26' toc_depth: 2 --- diff --git a/apps/www/_blog/2021-07-27-spot-flutter-with-postgres.mdx b/apps/www/_blog/2021-07-27-spot-flutter-with-postgres.mdx index c459d5edfa2..0bd7f8fce63 100644 --- a/apps/www/_blog/2021-07-27-spot-flutter-with-postgres.mdx +++ b/apps/www/_blog/2021-07-27-spot-flutter-with-postgres.mdx @@ -8,7 +8,7 @@ authorURL: https://github.com/roryw10 image: spot/og-spot-flutter-supabase.jpg thumb: spot/cover-spot-flutter-supabase.jpg tags: - - case-study + - flutter date: '2021-07-27' toc_depth: 2 --- diff --git a/apps/www/_blog/2021-07-28-mobbin-supabase-200000-users.mdx b/apps/www/_blog/2021-07-28-mobbin-supabase-200000-users.mdx index c4c623169c0..20afbe235dd 100644 --- a/apps/www/_blog/2021-07-28-mobbin-supabase-200000-users.mdx +++ b/apps/www/_blog/2021-07-28-mobbin-supabase-200000-users.mdx @@ -8,7 +8,6 @@ authorURL: https://github.com/roryw10 image: mobbin/og-mobbin-supabase.jpg thumb: mobbin/cover-mobbin-supabase.jpg tags: - - case-study - auth date: '2021-07-28' toc_depth: 2 diff --git a/apps/www/_blog/2021-10-19-replenysh-time-to-value-in-less-than-24-hours.mdx b/apps/www/_blog/2021-10-19-replenysh-time-to-value-in-less-than-24-hours.mdx index 1b83c2f4e73..cd36055543b 100644 --- a/apps/www/_blog/2021-10-19-replenysh-time-to-value-in-less-than-24-hours.mdx +++ b/apps/www/_blog/2021-10-19-replenysh-time-to-value-in-less-than-24-hours.mdx @@ -8,7 +8,6 @@ authorURL: https://github.com/roryw10 image: replenysh/og-replenysh.png thumb: replenysh/thumb-replenysh.png tags: - - case-study - auth date: '2021-10-19' toc_depth: 2 diff --git a/apps/www/_blog/2022-05-26-how-supabase-accelerates-development-of-all-pull-together.mdx b/apps/www/_blog/2022-05-26-how-supabase-accelerates-development-of-all-pull-together.mdx index 36fd3e53675..94d82b824b3 100644 --- a/apps/www/_blog/2022-05-26-how-supabase-accelerates-development-of-all-pull-together.mdx +++ b/apps/www/_blog/2022-05-26-how-supabase-accelerates-development-of-all-pull-together.mdx @@ -6,7 +6,6 @@ image: all-pull-together/all-pull-together-thumb.png thumb: all-pull-together/all-pull-together-thumb.png tags: - community - - case-study - q-and-a date: '2022-05-26' toc_depth: 3 diff --git a/apps/www/_blog/2023-02-16-case-study-happyteams.mdx b/apps/www/_blog/2023-02-16-case-study-happyteams.mdx index cd7366094c0..47c90329823 100644 --- a/apps/www/_blog/2023-02-16-case-study-happyteams.mdx +++ b/apps/www/_blog/2023-02-16-case-study-happyteams.mdx @@ -5,7 +5,6 @@ author: rory_wilding image: 2023-02-16-case-study-happyteams/case-study-happyteams.png thumb: 2023-02-16-case-study-happyteams/case-study-happyteams.png tags: - - case-study date: '2023-02-16' toc_depth: 3 --- diff --git a/apps/www/_blog/2023-06-27-native-mobile-auth.mdx b/apps/www/_blog/2023-06-27-native-mobile-auth.mdx new file mode 100644 index 00000000000..d9a3c7aeb43 --- /dev/null +++ b/apps/www/_blog/2023-06-27-native-mobile-auth.mdx @@ -0,0 +1,133 @@ +--- +title: Native Mobile Auth Support for Google and Apple Sign in +description: Supabase auth adds full support for native mobile sign in with Apple and Google. +author: stojan,tyler_shukert +image: native-mobile-auth/native-mobile-auth.png +thumb: native-mobile-auth/native-mobile-auth.png +tags: + - auth +date: '2023-06-27' +toc_depth: 3 +--- + +Supabase supports OAuth logins with 17 providers including Apple, Google, Microsoft, GitHub, … But for native mobile apps, this meant that developers had to use a web browser to sign in. It’s not an ideal flow for users, who are already used to signing in with the operating system’s native dialogs when possible. Today, we are excited to announce full native support for Sign in with Apple and Google on iOS and Android. But this is not all! Supabase Auth now can now be used with one-tap sign in methods like: Sign in with Apple JS, Sign in with Google for Web or even in Chrome extensions. + +## Native Sign in with Apple and Google + +Developers of native iOS and Android apps (using Flutter or React Native) can now take advantage of OS-provided authentication dialogs for Apple and Google. This is available on iOS, macOS, tvOS and watchOS apps in the Apple ecosystem, and all Android variants in the Google ecosystem. + +In full transparency, this was always sort-of possible but there were some edge cases that were not covered well with Supabase Auth. We’ve since ironed out the developer experience and made this into a fully supported feature. + +Behind the scenes, these native sign in methods use ID tokens. They’re a formalized version of a JWT that is issued by Apple or Google and contain profile information. Supabase Auth now can properly validate the ID tokens and create new or link to existing user accounts based on email similarity. + +### Using Sign in with Google in a Flutter App + +To setup Sign in with Google in your Flutter native app, you need to set up your Google Cloud project for each platform: + +- For iOS apps: [Get started with Google Sign In for iOS and macOS](https://developers.google.com/identity/sign-in/ios/start-integrating#get_an_oauth_client_id) +- For Android apps: [Get started with One Tap sign-in and sign-up for Android](https://developers.google.com/identity/one-tap/android/get-started) + +We’ve revamped the Google provider [configuration screen in the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers). Besides the existing OAuth flow, you can now add additional _Authorized Client IDs_ meant for native sign in. + +The client ID you obtained from the setup instructions above should be added to _Authorized Client IDs._ + +![Supabase dashboard Google provider](/images/blog/native-mobile-auth/google-setting.png) + +It really is as simple as that. + +### Using Sign in with Apple in a Flutter App + +We introduced support for native Sign in with Apple in our [previous launch week](https://supabase.com/blog/supabase-auth-sso-pkce#native-apple-login-on-ios#native-apple-login-on-ios), but today we are adding support for multiple apps on a single Supabase project. + +All you need to do is to register all of the bundle IDs of your apps in the [Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Authorized Client IDs_ as a comma separated string. + +![Supabase dashboard Apple provider](/images/blog/native-mobile-auth/apple-setting.png) + +## Using Sign in with Apple and Google in React Native Apps + +If you use React Native to build your native apps, you can still benefit. Please check out these resources to see how you can show the native authentication dialog and obtain an ID token from the operating system: + +- https://github.com/invertase/react-native-apple-authentication +- [Expo AppleAuthentication](https://docs.expo.dev/versions/latest/sdk/apple-authentication/) +- https://github.com/react-native-google-signin/google-signin + +You would still need to configure the _Authorized Client IDs_ as shown in the Flutter examples above. + +Finally, once you’ve received a valid ID token from the operating system you can call `supabase.auth.signInWithIdToken()` to complete the sign in with your Supabase project. + +For Sign in with Apple: + +```tsx +await supabase.auth.signInWithIdToken({ + provider: 'apple', + token: '', +}) +``` + +For Sign in with Google: + +```tsx +await supabase.auth.signInWithIdToken({ + provider: 'google', + token: '', +}) +``` + +We also have some sample implementation for Expo apps in our [create-t3-turbo example](https://github.com/supabase-community/create-t3-turbo/blob/main/apps/expo/src/app/profile.tsx#L47). + +## Using Sign in with Apple and Google in Flutter Apps + +Once you have configured your Supabase instance, you can utilize third party libraries like **[sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) or [google_one_tap_sign_in](https://pub.dev/packages/google_one_tap_sign_in) to perform naive sign in, and pass the ID token to Supabase to complete the sign in.** + +For Sign in with Apple: + +```dart +final credential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], +); + +await supabase.auth.signInWithIdToken( + provider: Provider.apple, + idToken: credential.identityToken!, +); +``` + +And for Google: + +```dart +var result = await GoogleOneTapSignIn.handleSignIn(webClientId: ''); + +supabase.auth.signInWithIdToken( + provider: Provider.google, + idToken: result.data!.idToken!, +); +``` + +You can find more detailed instructions on the [here](https://github.com/supabase/supabase-flutter#authentication). + +## Sign in with Apple JS, Google One Tap and Chrome Extensions + +Although sign in on native platforms was the focus of the team when working on this feature, incidentally we’ve added proper support for Sign in with Apple JS, Google’s One Tap and support for authenticating within Google Chrome extensions. + +You can now take advantage of these web frameworks, most notably Google’s One Tap and Automatic Sign-in support for a frictionless onboarding experience for your users. + +All you need to do is configure the web frameworks and use the `signInWithIdToken()` method to pass the ID token provided by the Google and Apple libraries. + +For example, to use Google One tap you should first [generate an embed code for the Google Sign in Button](https://developers.google.com/identity/gsi/web/tools/configurator). Register this method as the callback that will receive the authentication response from the button: + +```tsx +async function handleSignInWithGoogle(response) { + const { data, error } = await supabase.auth.signInWithIdToken({ + token: response.credential, + nonce: 'NONCE', // must be the same one as provided in data-nonce (if any) + }) +} +``` + +## Resources + +- [Login with Apple Guide](https://supabase.com/docs/guides/auth/social-login/auth-apple) +- [Login with Google Guide](https://supabase.com/docs/guides/auth/social-login/auth-google) diff --git a/apps/www/_customers/mendableai.mdx b/apps/www/_customers/mendableai.mdx index bcaa5e6cf50..44d70141b7e 100644 --- a/apps/www/_customers/mendableai.mdx +++ b/apps/www/_customers/mendableai.mdx @@ -34,8 +34,9 @@ about: Mendable is Chat Powered Search for Documentation. Mendable was experiencing tremendous success, growing Weekly Active Users nearly 300% since March. They needed a tool to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations. They tried Faiss, Weaviate, 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 discovered that Supabase supports pgvector 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. +## Why they chose Supabase + +Mendable discovered that Supabase supports [pgvector](https://supabase.com/docs/guides/database/extensions/pgvector) 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 Weaviate, we tried Pinecone. We found @@ -45,18 +46,18 @@ Mendable discovered that Supabase supports pgvector and found it to be a simple ## What They Built -Using Supabase and pgvector, Mendable was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, Mendable 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 Vector](https://supabase.com/vector), Mendable was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, Mendable 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 and pgvector, Mendable 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'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. + +## Tech stack + +Mendable's tech stack includes React, Next.js, Express, Vercel, and Supabase. diff --git a/apps/www/layouts/comparison.tsx b/apps/www/layouts/comparison.tsx index 0d55186d0f6..020d31e2559 100644 --- a/apps/www/layouts/comparison.tsx +++ b/apps/www/layouts/comparison.tsx @@ -72,7 +72,7 @@ const LayoutComparison = ({ components, props }: Props) => { // to do: author urls should be internal in future // currently we have external links to github profiles authors: [props.blog.author_url], - tags: props.blog.tags.map((cat: string) => { + tags: props.blog.tags?.map((cat: string) => { return cat }), }, diff --git a/apps/www/lib/posts.tsx b/apps/www/lib/posts.tsx index c0a7fad040f..82b0ba2df6a 100644 --- a/apps/www/lib/posts.tsx +++ b/apps/www/lib/posts.tsx @@ -78,7 +78,7 @@ export const getSortedPosts = ( if (tags) { sortedPosts = sortedPosts.filter((post: any) => { - const found = tags.some((tag: any) => post.tags.includes(tag)) + const found = tags.some((tag: any) => post.tags?.includes(tag)) return found }) } @@ -151,7 +151,7 @@ export const getAllCategories = (directory: Directories) => { posts.map((post: any) => { // add tags into categories array - post.tags.map((tag: string) => { + post.tags?.map((tag: string) => { if (!categories.includes(tag)) return categories.push(tag) }) }) diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 341fdabd2d4..e41861d6bcf 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -708,17 +708,17 @@ module.exports = [ { permanent: true, source: '/blog/2020/12/02/case-study-monitoro', - destination: '/blog/case-study-monitoro', + destination: '/customers', }, { permanent: true, source: '/blog/2020/12/02/case-study-tayfa', - destination: '/blog/case-study-tayfa', + destination: '/customers', }, { permanent: true, source: '/blog/2020/12/02/case-study-xendit', - destination: '/blog/case-study-xendit', + destination: '/customers/xendit', }, { permanent: true, @@ -743,7 +743,7 @@ module.exports = [ { permanent: true, source: '/blog/2021/02/09/case-study-roboflow', - destination: '/blog/case-study-roboflow', + destination: '/customers/roboflow', }, { permanent: true, @@ -763,7 +763,7 @@ module.exports = [ { permanent: true, source: '/blog/2021/03/08/toad-a-link-shortener-with-simple-apis-for-low-coders', - destination: '/blog/toad-a-link-shortener-with-simple-apis-for-low-coders', + destination: '/customers', }, { permanent: true, @@ -853,7 +853,7 @@ module.exports = [ { permanent: true, source: '/blog/2021/07/26/epsilon3-self-hosting', - destination: '/blog/epsilon3-self-hosting', + destination: '/customers/epsilon3', }, { permanent: true, @@ -868,7 +868,7 @@ module.exports = [ { permanent: true, source: '/blog/2021/07/27/spot-flutter-with-postgres', - destination: '/blog/spot-flutter-with-postgres', + destination: '/customers', }, { permanent: true, @@ -878,7 +878,7 @@ module.exports = [ { permanent: true, source: '/blog/2021/07/28/mobbin-supabase-200000-users', - destination: '/blog/mobbin-supabase-200000-users', + destination: '/customers/mobbin', }, { permanent: true, @@ -938,7 +938,7 @@ module.exports = [ { permanent: true, source: '/blog/2021/10/19/replenysh-time-to-value-in-less-than-24-hours', - destination: '/blog/replenysh-time-to-value-in-less-than-24-hours', + destination: '/customers/replenysh', }, { permanent: true, @@ -1103,7 +1103,7 @@ module.exports = [ { permanent: true, source: '/blog/2022/05/26/how-supabase-accelerates-development-of-all-pull-together', - destination: '/blog/how-supabase-accelerates-development-of-all-pull-together', + destination: '/customers', }, { permanent: true, @@ -1931,6 +1931,11 @@ module.exports = [ // old case studies moved to /customers + { + permanent: true, + source: '/blog/tags/case-study', + destination: '/customers', + }, { permanent: true, source: '/blog/case-study-monitoro', @@ -1983,8 +1988,8 @@ module.exports = [ }, { permanent: true, - source: '/blog/how-supabase-accelerates-development-of-all-pull-together', - destination: '/customers', + source: '/blog/case-study-happyteams', + destination: '/customers/happyteams', }, { permanent: true, diff --git a/apps/www/pages/blog.tsx b/apps/www/pages/blog.tsx index e5a4e1740c8..be68f700722 100644 --- a/apps/www/pages/blog.tsx +++ b/apps/www/pages/blog.tsx @@ -26,7 +26,7 @@ export async function getStaticProps() { fs.writeFileSync('./public/rss.xml', rss) // generate a series of rss feeds for each author (for PlanetPG) - const planetPgPosts = allPostsData.filter((post: any) => post.tags.includes('planetpg')) + const planetPgPosts = allPostsData.filter((post: any) => post.tags?.includes('planetpg')) const planetPgAuthors = planetPgPosts.map((post: any) => post.author.split(',')) const uniquePlanetPgAuthors = new Set([].concat(...planetPgAuthors)) @@ -73,7 +73,7 @@ function Blog(props: any) { category === 'all' ? shiftedBlogs : props.blogs.filter((post: any) => { - const found = post.tags.includes(category) + const found = post.tags?.includes(category) return found }) ) diff --git a/apps/www/pages/blog/[slug].tsx b/apps/www/pages/blog/[slug].tsx index e58397f1502..1b1fcbd5b8b 100644 --- a/apps/www/pages/blog/[slug].tsx +++ b/apps/www/pages/blog/[slug].tsx @@ -110,7 +110,7 @@ function BlogPostPage(props: any) {
- {props.blog.tags.map((tag: string) => { + {props.blog.tags?.map((tag: string) => { return ( {tag} @@ -164,7 +164,7 @@ function BlogPostPage(props: any) { // to do: author urls should be internal in future // currently we have external links to github profiles authors: [props.blog.author_url], - tags: props.blog.tags.map((cat: string) => { + tags: props.blog.tags?.map((cat: string) => { return cat }), }, diff --git a/apps/www/pages/customers/index.tsx b/apps/www/pages/customers/index.tsx index edda1a31441..171e61b53ba 100644 --- a/apps/www/pages/customers/index.tsx +++ b/apps/www/pages/customers/index.tsx @@ -50,7 +50,7 @@ function CustomerStoriesPage(props: any) { category === 'all' ? props.blogs : props.blogs.filter((post: any) => { - const found = post.tags.includes(category) + const found = post.tags?.includes(category) return found }) ) diff --git a/apps/www/pages/open-source/index.tsx b/apps/www/pages/open-source/index.tsx index e4f9b46e254..2ab0fa1f97a 100644 --- a/apps/www/pages/open-source/index.tsx +++ b/apps/www/pages/open-source/index.tsx @@ -94,7 +94,7 @@ const OpenSource = () => {
{maintainers - .filter((x) => activePill == 'All' || x.tags.includes(activePill)) + .filter((x) => activePill == 'All' || x.tags?.includes(activePill)) .sort((a, b) => a.handle.localeCompare(b.handle)) .map((x, idx) => (
diff --git a/apps/www/public/images/blog/native-mobile-auth/apple-setting.png b/apps/www/public/images/blog/native-mobile-auth/apple-setting.png new file mode 100644 index 00000000000..21911530722 Binary files /dev/null and b/apps/www/public/images/blog/native-mobile-auth/apple-setting.png differ diff --git a/apps/www/public/images/blog/native-mobile-auth/google-setting.png b/apps/www/public/images/blog/native-mobile-auth/google-setting.png new file mode 100644 index 00000000000..aa0e2bf5766 Binary files /dev/null and b/apps/www/public/images/blog/native-mobile-auth/google-setting.png differ diff --git a/apps/www/public/images/blog/native-mobile-auth/native-mobile-auth.png b/apps/www/public/images/blog/native-mobile-auth/native-mobile-auth.png new file mode 100644 index 00000000000..32ae2d8710f Binary files /dev/null and b/apps/www/public/images/blog/native-mobile-auth/native-mobile-auth.png differ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ae7e6c0c148..28c0406029f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -243,7 +243,7 @@ services: functions: container_name: supabase-edge-functions - image: supabase/edge-runtime:v1.4.2 + image: supabase/edge-runtime:v1.5.2 restart: unless-stopped environment: JWT_SECRET: ${JWT_SECRET} diff --git a/examples/todo-list/nextjs-todo-list/lib/initSupabase.ts b/examples/todo-list/nextjs-todo-list/lib/initSupabase.ts index 5457bdff41f..7827f47d5a9 100644 --- a/examples/todo-list/nextjs-todo-list/lib/initSupabase.ts +++ b/examples/todo-list/nextjs-todo-list/lib/initSupabase.ts @@ -1,7 +1,6 @@ import { createClient } from '@supabase/supabase-js' -import { Database } from './schema' export const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '' ) diff --git a/i18n/README.bn.md b/i18n/README.bn.md index 1e1c88ee112..10e03403aa2 100644 --- a/i18n/README.bn.md +++ b/i18n/README.bn.md @@ -43,7 +43,7 @@ - [x] পাবলিক বেটা: বেশিরভাগ নন-এন্টারপ্রাইজ ব্যবহারের ক্ষেত্রে যথেষ্ট স্থিতিশীল - [ ] পাবলিক: প্রোডাকশন রেডি -আমরা বর্তমানে পাবলিক বেটাতে আছি। বড় আপডেটের বিজ্ঞপ্তি পেতে এই রিপুর "রিলিজ" দেখুন। +আমরা বর্তমানে পাবলিক বিটাতে আছি। বড় আপডেটের বিজ্ঞপ্তি পেতে এই রেপোর "রিলিজ" দেখুন। এই রিপু দেখুন diff --git a/spec/examples/examples.yml b/spec/examples/examples.yml index 4960cd5d206..38933c31805 100644 --- a/spec/examples/examples.yml +++ b/spec/examples/examples.yml @@ -240,7 +240,7 @@ functions: js: | ```js const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'github' + provider: 'github', options: { redirectTo: 'https://example.com/welcome' } @@ -255,7 +255,7 @@ functions: js: | ```js const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'github' + provider: 'github', options: { scopes: 'repo gist notifications' } diff --git a/spec/supabase_js_v2.yml b/spec/supabase_js_v2.yml index a498046b33c..144d9b079b0 100644 --- a/spec/supabase_js_v2.yml +++ b/spec/supabase_js_v2.yml @@ -297,7 +297,7 @@ functions: code: | ```js const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'github' + provider: 'github', options: { redirectTo: 'https://example.com/welcome' } @@ -312,7 +312,7 @@ functions: code: | ```js const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'github' + provider: 'github', options: { scopes: 'repo gist notifications' } diff --git a/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index e78a998960d..cab31f81782 100644 --- a/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -151,19 +151,21 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { )} {provider.misc.requiresRedirect && ( <> - - {provider.misc.helper} - + {provider.misc.helper} + + } /> )} diff --git a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx index 1681ee1cdce..b46e60bd771 100644 --- a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx @@ -60,7 +60,7 @@ const Infrastructure = ({ const { data: cpuUsageData, isLoading: isLoadingCpuUsageData } = useInfraMonitoringQuery({ projectRef, - attribute: 'cpu_usage', + attribute: 'max_cpu_usage', interval, startDate, endDate, @@ -91,7 +91,7 @@ const Infrastructure = ({ ) const chartMeta: { [key: string]: { data: DataPoint[]; isLoading: boolean } } = { - cpu_usage: { + max_cpu_usage: { isLoading: isLoadingCpuUsageData, data: cpuUsageData?.data ?? [], }, @@ -226,7 +226,7 @@ const Infrastructure = ({
)} - {attribute.key === 'cpu_usage' && ( + {attribute.key === 'max_cpu_usage' && (

Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores.

diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx b/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx index c4d2b2c1386..abb971d2db1 100644 --- a/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx @@ -49,11 +49,11 @@ export const USAGE_CATEGORIES: CategoryMeta[] = [ attributes: [ { anchor: 'cpu', - key: 'cpu_usage', - attribute: 'cpu_usage', + key: 'max_cpu_usage', + attribute: 'max_cpu_usage', name: 'CPU', unit: 'percentage', - description: 'CPU usage of your server', + description: 'Max CPU usage of your server', chartDescription: '', links: [ { diff --git a/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index 6d5ad116972..522a3fae311 100644 --- a/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -17,7 +17,7 @@ import { tryParseJson, uuidv4 } from 'lib/helpers' import { Button, Checkbox, Form, Input, Listbox, Modal, Radio, SidePanel } from 'ui' import HTTPRequestFields from './HTTPRequestFields' import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants' -import { isValidHttpUrl } from './Hooks.utils' +import { isValidHttpUrl } from 'lib/helpers' export interface EditHookPanelProps { visible: boolean diff --git a/studio/components/interfaces/Database/Hooks/Hooks.utils.ts b/studio/components/interfaces/Database/Hooks/Hooks.utils.ts index 75f0d78c39a..43e8a40c57d 100644 --- a/studio/components/interfaces/Database/Hooks/Hooks.utils.ts +++ b/studio/components/interfaces/Database/Hooks/Hooks.utils.ts @@ -28,13 +28,3 @@ export const convertKeyValue = (value: string) => { export const hasWhitespace = (value: string) => { return /\s/.test(value) } - -export const isValidHttpUrl = (value: string) => { - let url: URL - try { - url = new URL(value) - } catch (_) { - return false - } - return url.protocol === 'http:' || url.protocol === 'https:' -} diff --git a/studio/components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails.tsx b/studio/components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails.tsx new file mode 100644 index 00000000000..16d4edac3f7 --- /dev/null +++ b/studio/components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails.tsx @@ -0,0 +1,33 @@ +export interface AuthorizeRequesterDetailsProps { + icon: string | null + name: string + domain: string +} + +const AuthorizeRequesterDetails = ({ icon, name, domain }: AuthorizeRequesterDetailsProps) => { + return ( +
+
+
+
+ {icon === null &&

{name[0]}

} +
+
+
+

+ {name} ({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. + +

+
+ ) +} + +export default AuthorizeRequesterDetails diff --git a/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx b/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx new file mode 100644 index 00000000000..d8b4360ef66 --- /dev/null +++ b/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx @@ -0,0 +1,57 @@ +import Table from 'components/to-be-cleaned/Table' +import { AuthorizedApp } from 'data/oauth/authorized-apps-query' +import dayjs from 'dayjs' +import { copyToClipboard } from 'lib/helpers' +import { useState } from 'react' +import { Button, IconCheck, IconClipboard, IconTrash } from 'ui' + +export interface AuthorizedAppRowProps { + app: AuthorizedApp + onSelectRevoke: () => void +} + +const AuthorizedAppRow = ({ app, onSelectRevoke }: AuthorizedAppRowProps) => { + const [isCopied, setIsCopied] = useState(false) + + return ( + + +
+ {!!app.icon ? '' : `${app.name[0]}`} +
+
+ {app.name} + + {app.id} + +
+ +
+ {isLoadingPublishedApps && ( +
+ + + +
+ )} + + {isErrorPublishedApps && ( + + )} + + {createdApp !== undefined && ( + +
+
+
+

+ Ensure that you store the client secret securely - you will not be able to see + it again. +

+
+ {}} + /> + {}} + /> +
+
+
+ )} + + {isSuccessPublishedApps && ( + <> + {(publishedApps?.length ?? 0) === 0 ? ( +
+

You do not have any published applications yet

+
+ ) : ( + , + Name, + Client ID, + Client Secret, + Created at, + , + ]} + body={ + sortedPublishedApps?.map((app) => ( + { + setShowPublishModal(true) + setSelectedAppToUpdate(app) + }} + onSelectDelete={() => setSelectedAppToDelete(app)} + /> + )) ?? [] + } + /> + )} + + )} + + + +
+

Authorized Apps

+

+ Applications that have access to your organization's settings and projects +

+ +
+ {isLoadingAuthorizedApps && ( +
+ + + +
+ )} + + {isErrorAuthorizedApps && } + + {isSuccessAuthorizedApps && ( + <> + {(authorizedApps.length ?? 0) === 0 ? ( +
+

You do not have any authorized applications yet

+
+ ) : ( +
, + Name, + ID, + Authorized at, + , + ]} + body={ + sortedAuthorizedApps?.map((app) => ( + setSelectedAppToRevoke(app)} + /> + )) ?? [] + } + /> + )} + + )} + + + + + { + setSelectedAppToUpdate(undefined) + setShowPublishModal(false) + }} + onCreateSuccess={setCreatedApp} + /> + setSelectedAppToDelete(undefined)} + /> + setSelectedAppToRevoke(undefined)} + /> + + ) +} + +export default OAuthApps diff --git a/studio/components/interfaces/Organization/OAuthApps/PublishAppSidePanel.tsx b/studio/components/interfaces/Organization/OAuthApps/PublishAppSidePanel.tsx new file mode 100644 index 00000000000..43eaa6e9044 --- /dev/null +++ b/studio/components/interfaces/Organization/OAuthApps/PublishAppSidePanel.tsx @@ -0,0 +1,397 @@ +import clsx from 'clsx' +import { ChangeEvent, useEffect, useRef, useState } from 'react' + +import { useParams } from 'common' +import { + OAuthAppCreateResponse, + useOAuthAppCreateMutation, +} from 'data/oauth/oauth-app-create-mutation' +import { useOAuthAppUpdateMutation } from 'data/oauth/oauth-app-update-mutation' +import { OAuthApp } from 'data/oauth/oauth-apps-query' +import { useStore } from 'hooks' +import { isValidHttpUrl, uuidv4 } from 'lib/helpers' +import { Badge, Button, Dropdown, Form, IconEdit, IconUpload, Input, Modal, SidePanel } from 'ui' +import { uploadAttachment } from 'lib/upload' +import AuthorizeRequesterDetails from './AuthorizeRequesterDetails' + +export interface PublishAppModalProps { + visible: boolean + selectedApp?: OAuthApp + onClose: () => void + onCreateSuccess: (app: OAuthAppCreateResponse) => void +} + +const PublishAppModal = ({ + visible, + selectedApp, + onClose, + onCreateSuccess, +}: PublishAppModalProps) => { + const { ui } = useStore() + const { slug } = useParams() + const uploadButtonRef = useRef() + const { mutateAsync: createOAuthApp } = useOAuthAppCreateMutation() + const { mutateAsync: updateOAuthApp } = useOAuthAppUpdateMutation() + + const [showPreview, setShowPreview] = useState(false) + const [iconFile, setIconFile] = useState() + const [iconUrl, setIconUrl] = useState() + const [errors, setErrors] = useState<{ [key: string]: string }>({}) + const [urls, setUrls] = useState<{ id: string; value: string }[]>([{ id: uuidv4(), value: '' }]) + + useEffect(() => { + if (visible) { + setErrors({}) + setIconFile(undefined) + + if (selectedApp !== undefined) { + setUrls( + selectedApp.redirect_uris.map((url) => { + return { id: uuidv4(), value: url } + }) + ) + setIconUrl(selectedApp.icon === null ? undefined : selectedApp.icon) + } else { + setUrls([{ id: uuidv4(), value: '' }]) + setIconUrl(undefined) + } + } + }, [visible, selectedApp]) + + const onUpdateUrl = (id: string, value: string) => { + const updatedUrls = urls.map((url) => { + if (url.id === id) return { id, value } + else return url + }) + setUrls(updatedUrls) + setErrors({}) + } + + const removeUrl = (id: string) => { + const updatedUrls = urls.filter((url) => url.id !== id) + setUrls(updatedUrls) + } + + const onFileUpload = async (event: ChangeEvent) => { + event.persist() + const [file] = event.target.files || (event as any).dataTransfer.items + setIconFile(file) + setIconUrl(URL.createObjectURL(file)) + event.target.value = '' + } + + const validate = (values: any) => { + const errors: any = {} + if (!values.name) errors.name = 'Please provide a name for your application' + if (!values.website) errors.website = 'Please provide a URL for your site' + if (!isValidHttpUrl(values.website)) errors.website = 'Please provide a valid URL for your site' + return errors + } + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (!slug) return console.error('Slug is required') + + const redirect_uris = urls.filter((url) => url.value.length > 0).map((url) => url.value) + if (redirect_uris.length === 0) { + setSubmitting(false) + setErrors({ urls: 'Please provide at least one URL' }) + return + } else { + setErrors({}) + } + + const { name, website } = values + const uploadedIconUrl = + iconFile !== undefined + ? await uploadAttachment('oauth-app-icons', `${slug}/${uuidv4()}.png`, iconFile) + : iconUrl + + if (selectedApp === undefined) { + // Create application + try { + const res = await createOAuthApp({ + slug, + name, + website, + redirect_uris, + icon: uploadedIconUrl, + }) + ui.setNotification({ + category: 'success', + message: `Successfully created OAuth app "${name}"!`, + }) + onClose() + onCreateSuccess(res) + } catch (error: any) { + setSubmitting(false) + ui.setNotification({ + category: 'error', + message: `Failed to create OAuth app: ${error.message}`, + }) + } + } else { + // Update application + try { + await updateOAuthApp({ + id: selectedApp.id, + slug, + name, + website, + redirect_uris, + icon: uploadedIconUrl === undefined ? null : uploadedIconUrl, + }) + ui.setNotification({ + category: 'success', + message: `Successfully updated OAuth app "${name}"!`, + }) + onClose() + } catch (error: any) { + setSubmitting(false) + ui.setNotification({ + category: 'error', + message: `Failed to update OAuth app: ${error.message}`, + }) + } + } + } + + return ( + <> + onClose()} + > +
+ {({ + isSubmitting, + resetForm, + values, + }: { + isSubmitting: boolean + resetForm: any + values: any + }) => { + // [Joshen] although this "technically" is breaking the rules of React hooks + // it won't error because the hooks are always rendered in the same order + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (visible && selectedApp !== undefined) { + const values = { name: selectedApp.name, website: selectedApp.website } + resetForm({ values, initialValues: values }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible, selectedApp]) + + return ( + <> +
+
+ +
+
+ + +
+
+ {iconUrl !== undefined ? ( +
+
+ { + if (uploadButtonRef.current) + (uploadButtonRef.current as any).click() + }} + > + Upload image + , + { + setIconFile(undefined) + setIconUrl(undefined) + }} + > + Remove image + , + ]} + > +
+
+ ) : ( +
{ + if (uploadButtonRef.current) + (uploadButtonRef.current as any).click() + }} + > + +

Upload logo

+
+ )} + +
+
+
+ + + + +
+
+

Authorization callback URLs

+

+ All URLs must use HTTPS, except for localhost +

+
+ +
+
+ {urls.map((url) => ( + onUpdateUrl(url.id, event.target.value)} + placeholder="e.g https://my-website.com" + actions={[ + urls.length > 1 ? ( + + ) : null, + ]} + /> + ))} + {errors.urls &&

{errors.urls}

} +
+
+ +
+ + + + +
+ +
+ + +
+
+
+
+ + setShowPreview(false)} + > + +
+

Authorize API access for {values.name}

+ Preview +
+
+ + +
+ +
+

+ Select an organization to grant API access to +

+
+ Organizations that you have access to will be listed here +
+
+
+
+ + +
+

+ This is what your users will see when authorizing with your app +

+ +
+
+
+ + ) + }} + +
+ + ) +} + +export default PublishAppModal diff --git a/studio/components/interfaces/Organization/OAuthApps/RevokeAppModal.tsx b/studio/components/interfaces/Organization/OAuthApps/RevokeAppModal.tsx new file mode 100644 index 00000000000..7d49e8d54c0 --- /dev/null +++ b/studio/components/interfaces/Organization/OAuthApps/RevokeAppModal.tsx @@ -0,0 +1,73 @@ +import { useParams } from 'common' +import { useAuthorizedAppRevokeMutation } from 'data/oauth/authorized-app-revoke-mutation' +import { AuthorizedApp } from 'data/oauth/authorized-apps-query' +import { useStore } from 'hooks' +import { Alert, IconAlertOctagon, IconLock, Modal } from 'ui' + +export interface RevokeAppModalProps { + selectedApp?: AuthorizedApp + onClose: () => void +} + +const RevokeAppModal = ({ selectedApp, onClose }: RevokeAppModalProps) => { + const { ui } = useStore() + const { slug } = useParams() + const { mutateAsync: revokeAuthorizedApp, isLoading: isDeleting } = + useAuthorizedAppRevokeMutation() + + const onConfirmDelete = async () => { + if (!slug) return console.error('Slug is required') + if (!selectedApp?.id) return console.error('App ID is required') + + try { + await revokeAuthorizedApp({ slug, id: selectedApp?.id }) + ui.setNotification({ + category: 'success', + message: `Successfully revoked the app "${selectedApp?.name}"`, + }) + onClose() + } catch (error) { + ui.setNotification({ + category: 'error', + message: `Failed to revoke app: ${(error as any).message}`, + }) + } + } + + return ( + + +
+ + {selectedApp?.name} application will no longer have access to your organization's + settings and projects. + +
    +
  • + +
    + Before you remove this app, consider: +
      +
    • + No users are currently using this application. The application will no longer + have access to your organization after being revoked. +
    • +
    +
    +
  • +
+
+
+
+ ) +} + +export default RevokeAppModal diff --git a/studio/components/interfaces/Organization/index.ts b/studio/components/interfaces/Organization/index.ts index beee07cbc43..2c02467f0f9 100644 --- a/studio/components/interfaces/Organization/index.ts +++ b/studio/components/interfaces/Organization/index.ts @@ -3,5 +3,6 @@ import TeamSettings from './TeamSettings/TeamSettings' import BillingSettings from './BillingSettings/BillingSettings' import InvoicesSettings from './InvoicesSettings/InvoicesSettings' import NewOrgForm from './NewOrg/NewOrgForm' +import OAuthApps from './OAuthApps/OAuthApps' -export { GeneralSettings, TeamSettings, BillingSettings, InvoicesSettings, NewOrgForm } +export { GeneralSettings, TeamSettings, BillingSettings, InvoicesSettings, NewOrgForm, OAuthApps } diff --git a/studio/components/interfaces/Settings/Logs/Logs.utils.ts b/studio/components/interfaces/Settings/Logs/Logs.utils.ts index 006f78c3ff9..f8ff4d249a1 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.utils.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.utils.ts @@ -234,7 +234,7 @@ export const genChartQuery = ( const where = _genWhereStatement(table, filters) let joins = 'cross join unnest(t.metadata) as metadata' - if (table === LogsTableName.EDGE) { + if (table === LogsTableName.EDGE || table === LogsTableName.FN_EDGE) { joins += ' \n cross join unnest(metadata.request) as request' joins += ' \n cross join unnest(metadata.response) as response' } else if (table === LogsTableName.POSTGRES) { diff --git a/studio/components/interfaces/SignIn/SignInForm.tsx b/studio/components/interfaces/SignIn/SignInForm.tsx index 4d20f3fa307..cd8be134729 100644 --- a/studio/components/interfaces/SignIn/SignInForm.tsx +++ b/studio/components/interfaces/SignIn/SignInForm.tsx @@ -37,7 +37,7 @@ const SignInForm = () => { const signInClicks = incrementSignInClicks() if (signInClicks > 1) { - Sentry.captureMessage('Sign in without previous sing out detected') + Sentry.captureMessage('Sign in without previous sign out detected') } const { error } = await auth.signInWithPassword({ diff --git a/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx index d23ab9333c0..c83802db398 100644 --- a/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx +++ b/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx @@ -83,19 +83,20 @@ const SideBarContent = observer(() => {
{IS_PLATFORM && ( -
-
+
+
handleNewQuery()}>
-

New blank query

+

New blank query

, +
, showCmdkHelper ? ( { }} >
-

New AI query

+

New AI query

) : null, @@ -114,7 +115,8 @@ const SideBarContent = observer(() => { + + , + ]} + > + Try refreshing your browser, but if the issue persists, please reach out to us via support. + + ) +} + +export default AlertError diff --git a/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx b/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx index 3029463350c..fce70a3dc2b 100644 --- a/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx +++ b/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx @@ -51,7 +51,6 @@ const DisplayApiSettings = () => { Client Docs - .

} diff --git a/studio/data/analytics/infra-monitoring-query.ts b/studio/data/analytics/infra-monitoring-query.ts index 642baeacfb8..1783de01939 100644 --- a/studio/data/analytics/infra-monitoring-query.ts +++ b/studio/data/analytics/infra-monitoring-query.ts @@ -8,7 +8,13 @@ import { analyticsKeys } from './keys' export type InfraMonitoringVariables = { projectRef?: string - attribute: 'cpu_usage' | 'disk_io_budget' | 'ram_usage' | 'disk_io_consumption' | 'swap_usage' + attribute: + | 'max_cpu_usage' + | 'avg_cpu_usage' + | 'disk_io_budget' + | 'ram_usage' + | 'disk_io_consumption' + | 'swap_usage' startDate?: string endDate?: string interval?: '1m' | '5m' | '10m' | '30m' | '1h' | '1d' diff --git a/studio/data/oauth/authorized-app-revoke-mutation.ts b/studio/data/oauth/authorized-app-revoke-mutation.ts new file mode 100644 index 00000000000..d47499a4a77 --- /dev/null +++ b/studio/data/oauth/authorized-app-revoke-mutation.ts @@ -0,0 +1,44 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { delete_ } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { oauthAppKeys } from './keys' + +export type AuthorizedAppRevokeVariables = { + id: string + slug: string +} + +export async function revokeAuthorizedApp({ id, slug }: AuthorizedAppRevokeVariables) { + if (!id) throw new Error('App ID is required') + if (!slug) throw new Error('Organization slug is required') + + const response = await delete_( + `${API_ADMIN_URL}/organizations/${slug}/oauth/apps/${id}?type=authorized` + ) + if (response.error) throw response.error + return response +} + +type AuthorizedAppRevokeData = Awaited> + +export const useAuthorizedAppRevokeMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => revokeAuthorizedApp(vars), + { + async onSuccess(data, variables, context) { + const { slug } = variables + await queryClient.invalidateQueries(oauthAppKeys.authorizedApps(slug)) + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/oauth/authorized-apps-query.ts b/studio/data/oauth/authorized-apps-query.ts new file mode 100644 index 00000000000..6c37cdaf441 --- /dev/null +++ b/studio/data/oauth/authorized-apps-query.ts @@ -0,0 +1,59 @@ +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 { oauthAppKeys } from './keys' +import { ResponseError } from 'types' + +export type AuthorizedAppsVariables = { + slug?: string +} + +export type AuthorizedApp = { + id: string + icon: string | null + name: string + website: string + authorized_at: string +} + +export async function getAuthorizedApps({ slug }: AuthorizedAppsVariables, signal?: AbortSignal) { + if (!slug) throw new Error('Organization slug is required') + + const response = await get(`${API_ADMIN_URL}/organizations/${slug}/oauth/apps?type=authorized`, { + signal, + }) + if (response.error) throw response.error + return response as AuthorizedApp[] +} + +export type AuthorizedAppsData = Awaited> +export type AuthorizedAppsError = ResponseError + +export const useAuthorizedAppsQuery = ( + { slug }: AuthorizedAppsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + oauthAppKeys.authorizedApps(slug), + ({ signal }) => getAuthorizedApps({ slug }, signal), + { + enabled: enabled && typeof slug !== 'undefined', + ...options, + } + ) + +export const useAuthorizedAppsPrefetch = ({ slug }: AuthorizedAppsVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (slug) { + client.prefetchQuery(oauthAppKeys.authorizedApps(slug), ({ signal }) => + getAuthorizedApps({ slug }, signal) + ) + } + }, [slug]) +} diff --git a/studio/data/oauth/keys.ts b/studio/data/oauth/keys.ts new file mode 100644 index 00000000000..53b02310942 --- /dev/null +++ b/studio/data/oauth/keys.ts @@ -0,0 +1,4 @@ +export const oauthAppKeys = { + oauthApps: (slug: string | undefined) => [slug, 'oauth-apps'] as const, + authorizedApps: (slug: string | undefined) => [slug, 'authorized-apps'] as const, +} diff --git a/studio/data/oauth/oauth-app-create-mutation.ts b/studio/data/oauth/oauth-app-create-mutation.ts new file mode 100644 index 00000000000..14152e5472f --- /dev/null +++ b/studio/data/oauth/oauth-app-create-mutation.ts @@ -0,0 +1,63 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { oauthAppKeys } from './keys' + +export type OAuthAppCreateVariables = { + slug: string + name: string + website: string + icon?: string + redirect_uris: string[] +} + +export type OAuthAppCreateResponse = { + id: string + client_id: string + client_secret: string +} + +export async function createOAuthApp({ + slug, + name, + website, + icon, + redirect_uris, +}: OAuthAppCreateVariables) { + if (!slug) throw new Error('Organization slug is required') + if (!name) throw new Error('OAuth app name is required') + if (!website) throw new Error('OAuth app URL is required') + + const response = await post(`${API_ADMIN_URL}/organizations/${slug}/oauth/apps`, { + name, + website, + icon, + redirect_uris, + }) + if (response.error) throw response.error + return response as OAuthAppCreateResponse +} + +type OAuthAppCreateData = Awaited> + +export const useOAuthAppCreateMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => createOAuthApp(vars), + { + async onSuccess(data, variables, context) { + const { slug } = variables + await queryClient.invalidateQueries(oauthAppKeys.oauthApps(slug)) + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/oauth/oauth-app-delete-mutation.ts b/studio/data/oauth/oauth-app-delete-mutation.ts new file mode 100644 index 00000000000..28592be674b --- /dev/null +++ b/studio/data/oauth/oauth-app-delete-mutation.ts @@ -0,0 +1,44 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { delete_ } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { oauthAppKeys } from './keys' + +export type OAuthAppDeleteVariables = { + id: string + slug: string +} + +export async function deleteOAuthApp({ id, slug }: OAuthAppDeleteVariables) { + if (!id) throw new Error('OAuth app ID is required') + if (!slug) throw new Error('Organization slug is required') + + const response = await delete_( + `${API_ADMIN_URL}/organizations/${slug}/oauth/apps/${id}?type=published` + ) + if (response.error) throw response.error + return response +} + +type OAuthAppDeleteData = Awaited> + +export const useOAuthAppDeleteMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => deleteOAuthApp(vars), + { + async onSuccess(data, variables, context) { + const { slug } = variables + await queryClient.invalidateQueries(oauthAppKeys.oauthApps(slug)) + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/oauth/oauth-app-update-mutation.ts b/studio/data/oauth/oauth-app-update-mutation.ts new file mode 100644 index 00000000000..acc4ab79f3b --- /dev/null +++ b/studio/data/oauth/oauth-app-update-mutation.ts @@ -0,0 +1,61 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { put } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { oauthAppKeys } from './keys' + +export type OAuthAppUpdateVariables = { + id: string + slug: string + name: string + website: string + icon?: string | null + redirect_uris: string[] +} + +export async function updateOAuthApp({ + id, + slug, + name, + website, + icon, + redirect_uris, +}: OAuthAppUpdateVariables) { + if (!id) throw new Error('OAuth app ID is required') + if (!slug) throw new Error('Organization slug is required') + if (!name) throw new Error('OAuth app name is required') + if (!website) throw new Error('OAuth app URL is required') + if (!redirect_uris || redirect_uris.length === 0) throw new Error('Redirect URIs are required') + + const response = await put(`${API_ADMIN_URL}/organizations/${slug}/oauth/apps/${id}`, { + name, + website, + icon, + redirect_uris, + }) + if (response.error) throw response.error + return response +} + +type OAuthAppUpdateData = Awaited> + +export const useOAuthAppUpdateMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => updateOAuthApp(vars), + { + async onSuccess(data, variables, context) { + const { slug } = variables + await queryClient.invalidateQueries(oauthAppKeys.oauthApps(slug)) + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/oauth/oauth-apps-query.ts b/studio/data/oauth/oauth-apps-query.ts new file mode 100644 index 00000000000..5dc6a229ce1 --- /dev/null +++ b/studio/data/oauth/oauth-apps-query.ts @@ -0,0 +1,59 @@ +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 { oauthAppKeys } from './keys' +import { ResponseError } from 'types' + +export type OAuthAppsVariables = { + slug?: string +} + +export type OAuthApp = { + id: string + icon: string | null + client_id: string + client_secret_alias: string + created_at: string + name: string + website: string + redirect_uris: string[] +} + +export async function getOAuthApps({ slug }: OAuthAppsVariables, signal?: AbortSignal) { + if (!slug) throw new Error('Organization slug is required') + + const response = await get(`${API_ADMIN_URL}/organizations/${slug}/oauth/apps?type=published`, { + signal, + }) + if (response.error) throw response.error + return response as OAuthApp[] +} + +export type OAuthAppsData = Awaited> +export type OAuthAppsError = ResponseError + +export const useOAuthAppsQuery = ( + { slug }: OAuthAppsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + oauthAppKeys.oauthApps(slug), + ({ signal }) => getOAuthApps({ slug }, signal), + { + enabled: enabled && typeof slug !== 'undefined', + ...options, + } + ) + +export const useOAuthAppsPrefetch = ({ slug }: OAuthAppsVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (slug) { + client.prefetchQuery(oauthAppKeys.oauthApps(slug), ({ signal }) => + getOAuthApps({ slug }, signal) + ) + } + }, [slug]) +} diff --git a/studio/lib/constants/metrics.tsx b/studio/lib/constants/metrics.tsx index 9ab4fb3374d..360ac5352e4 100644 --- a/studio/lib/constants/metrics.tsx +++ b/studio/lib/constants/metrics.tsx @@ -40,8 +40,14 @@ export const METRIC_CATEGORIES = { export const METRICS = [ { - key: 'cpu_usage', - label: 'CPU % usage', + key: 'avg_cpu_usage', + label: 'Average CPU % usage', + provider: 'infra-monitoring', + category: METRIC_CATEGORIES.INSTANCE, + }, + { + key: 'max_cpu_usage', + label: 'Max CPU % usage', provider: 'infra-monitoring', category: METRIC_CATEGORIES.INSTANCE, }, diff --git a/studio/lib/helpers.ts b/studio/lib/helpers.ts index 65c39154eda..b5d09d62ab9 100644 --- a/studio/lib/helpers.ts +++ b/studio/lib/helpers.ts @@ -246,3 +246,13 @@ export const detectOS = () => { return undefined } } + +export const isValidHttpUrl = (value: string) => { + let url: URL + try { + url = new URL(value) + } catch (_) { + return false + } + return url.protocol === 'http:' || url.protocol === 'https:' +} diff --git a/studio/lib/upload.ts b/studio/lib/upload.ts new file mode 100644 index 00000000000..5f141374099 --- /dev/null +++ b/studio/lib/upload.ts @@ -0,0 +1,45 @@ +import { createClient } from '@supabase/supabase-js' + +const SUPPORT_API_URL = process.env.NEXT_PUBLIC_SUPPORT_API_URL || '' +const SUPPORT_API_KEY = process.env.NEXT_PUBLIC_SUPPORT_ANON_KEY || '' + +// [Joshen TODO] Feedback form and support attachments should use this +export const uploadAttachment = async ( + bucket: string, + fileName: string, + image: File, + getUrl: boolean = true +) => { + const supabaseClient = createClient(SUPPORT_API_URL, SUPPORT_API_KEY, { + auth: { + persistSession: false, + autoRefreshToken: false, + // @ts-ignore + multiTab: false, + detectSessionInUrl: false, + localStorage: { + getItem: (key: string) => undefined, + setItem: (key: string, value: string) => {}, + removeItem: (key: string) => {}, + }, + }, + }) + + const options = { cacheControl: '3600' } + + const { data: file, error } = await supabaseClient.storage + .from(bucket) + .upload(fileName, image, options) + + if (error) { + console.error('Failed to upload:', error) + return undefined + } + + if (file && getUrl) { + const { data } = await supabaseClient.storage.from(bucket).getPublicUrl(file.path) + return data?.publicUrl + } + + return undefined +} diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index a7f1c81e920..4d28fb24a67 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -1,4 +1,9 @@ +import dayjs from 'dayjs' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' + import { useParams } from 'common' +import AuthorizeRequesterDetails from 'components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails' import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout' import { FormPanel } from 'components/ui/Forms' import ShimmeringLoader from 'components/ui/ShimmeringLoader' @@ -6,10 +11,7 @@ import { useApiAuthorizationApproveMutation } from 'data/api-authorization/api-a 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' @@ -170,30 +172,11 @@ const APIAuthorizationPage: NextPageWithLayout = () => { >
{/* API Authorization requester details */} - -
-
-
-
- {requester?.icon === null && ( -

{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 && ( diff --git a/studio/pages/org/[slug]/apps.tsx b/studio/pages/org/[slug]/apps.tsx new file mode 100644 index 00000000000..95b26ceb47e --- /dev/null +++ b/studio/pages/org/[slug]/apps.tsx @@ -0,0 +1,57 @@ +import { useRouter } from 'next/router' +import { observer } from 'mobx-react-lite' +import { Tabs } from 'ui' + +import { NextPageWithLayout } from 'types' +import { useStore } from 'hooks' +import { useParams } from 'common/hooks' +import Loading from 'components/ui/Loading' +import { OrganizationLayout } from 'components/layouts' +import { OAuthApps } from 'components/interfaces/Organization' + +const OrgOAuthApps: NextPageWithLayout = () => { + const { ui } = useStore() + const { slug } = useParams() + const router = useRouter() + + return ( + <> + {ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? ( + + ) : ( +
+
+
+

+ {ui.selectedOrganization?.name ?? 'Organization'} settings +

+
+ +
+ +
+ +
+
+ )} + + ) +} + +OrgOAuthApps.getLayout = (page) => {page} +export default observer(OrgOAuthApps) diff --git a/studio/pages/org/[slug]/billing.tsx b/studio/pages/org/[slug]/billing.tsx index 774a22c49c0..d32cfa72bc1 100644 --- a/studio/pages/org/[slug]/billing.tsx +++ b/studio/pages/org/[slug]/billing.tsx @@ -5,7 +5,7 @@ import { BillingSettings } from 'components/interfaces/Organization' import { OrganizationLayout } from 'components/layouts' import Loading from 'components/ui/Loading' import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useSelectedOrganization } from 'hooks' +import { useFlag, useSelectedOrganization } from 'hooks' import { NextPageWithLayout } from 'types' import { Tabs } from 'ui' @@ -14,6 +14,7 @@ const OrgBillingSettings: NextPageWithLayout = () => { const selectedOrganization = useSelectedOrganization() const { slug } = useParams() const router = useRouter() + const showOAuthApps = useFlag('oauthApps') return ( <> @@ -38,6 +39,7 @@ const OrgBillingSettings: NextPageWithLayout = () => { + {showOAuthApps && }
diff --git a/studio/pages/org/[slug]/general.tsx b/studio/pages/org/[slug]/general.tsx index a37e32a78e8..6ac6f02f674 100644 --- a/studio/pages/org/[slug]/general.tsx +++ b/studio/pages/org/[slug]/general.tsx @@ -5,7 +5,7 @@ import { GeneralSettings } from 'components/interfaces/Organization' import { OrganizationLayout } from 'components/layouts' import Loading from 'components/ui/Loading' import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useSelectedOrganization } from 'hooks' +import { useFlag, useSelectedOrganization } from 'hooks' import { NextPageWithLayout } from 'types' import { Tabs } from 'ui' @@ -14,6 +14,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => { const selectedOrganization = useSelectedOrganization() const { slug } = useParams() const router = useRouter() + const showOAuthApps = useFlag('oauthApps') return ( <> @@ -38,6 +39,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => { + {showOAuthApps && }
diff --git a/studio/pages/org/[slug]/invoices.tsx b/studio/pages/org/[slug]/invoices.tsx index ad95c46b449..4219e34c41d 100644 --- a/studio/pages/org/[slug]/invoices.tsx +++ b/studio/pages/org/[slug]/invoices.tsx @@ -7,7 +7,7 @@ import { InvoicesSettings } from 'components/interfaces/Organization' import { OrganizationLayout } from 'components/layouts' import Loading from 'components/ui/Loading' import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useSelectedOrganization } from 'hooks' +import { useFlag, useSelectedOrganization } from 'hooks' import { NextPageWithLayout } from 'types' const OrgInvoices: NextPageWithLayout = () => { @@ -15,6 +15,7 @@ const OrgInvoices: NextPageWithLayout = () => { const selectedOrganization = useSelectedOrganization() const { slug } = useParams() const router = useRouter() + const showOAuthApps = useFlag('oauthApps') return ( <> @@ -39,6 +40,7 @@ const OrgInvoices: NextPageWithLayout = () => { + {showOAuthApps && }
diff --git a/studio/pages/org/[slug]/team.tsx b/studio/pages/org/[slug]/team.tsx index c3c2200b33a..8d024a5652f 100644 --- a/studio/pages/org/[slug]/team.tsx +++ b/studio/pages/org/[slug]/team.tsx @@ -5,7 +5,7 @@ import { TeamSettings } from 'components/interfaces/Organization' import { OrganizationLayout } from 'components/layouts' import Loading from 'components/ui/Loading' import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useSelectedOrganization } from 'hooks' +import { useFlag, useSelectedOrganization } from 'hooks' import { NextPageWithLayout } from 'types' import { Tabs } from 'ui' @@ -14,6 +14,7 @@ const OrgTeamSettings: NextPageWithLayout = () => { const selectedOrganization = useSelectedOrganization() const { slug } = useParams() const router = useRouter() + const showOAuthApps = useFlag('oauthApps') return ( <> @@ -38,6 +39,7 @@ const OrgTeamSettings: NextPageWithLayout = () => { + {showOAuthApps && }
diff --git a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx index 750ff44fa62..3020af6abec 100644 --- a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx +++ b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx @@ -1,6 +1,10 @@ -import { boolean, number, object, string } from 'yup' +import { boolean, number, object, string, ValidationError } from 'yup' import { urlRegex } from 'components/interfaces/Auth/Auth.constants' +const parseBase64URL = (b64url: string) => { + return atob(b64url.replace(/[-]/g, '+').replace(/[_]/g, '/')) +} + const JSON_SCHEMA_VERSION = 'http://json-schema.org/draft-07/schema#' const PROVIDER_EMAIL = { @@ -300,80 +304,139 @@ const EXTERNAL_PROVIDER_APPLE = { title: 'Apple', properties: { EXTERNAL_APPLE_ENABLED: { - title: 'Enable Apple provider', - description: 'This will enable Apple login for your application', + title: 'Enable Sign in with Apple', + description: + 'Enables Sign in with Apple on the web using OAuth or natively within iOS, macOS, watchOS or tvOS apps.', type: 'boolean', }, EXTERNAL_APPLE_CLIENT_ID: { - /** - * to do: change docs - */ - title: 'Services ID', - description: ` -Client identifier when authenticating or validating users. + title: 'Service ID (for OAuth)', + description: `Client identifier used in the OAuth flow on the web. [Learn more](https://developer.apple.com/documentation/sign_in_with_apple/configuring_your_environment_for_sign_in_with_apple)`, type: 'string', }, - EXTERNAL_IOS_BUNDLE_ID: { - /** - * to do: change docs - */ - title: 'IOS Bundle ID', - description: `The iOS app's unique identifier. [Learn more](https://developer.apple.com/documentation/appstoreconnectapi/bundle_ids)`, - type: 'string', - }, EXTERNAL_APPLE_SECRET: { - /** - * to do: change docs - */ - title: 'Secret key', - description: ` -The secret key is a JWT token that must be generated. + title: 'Secret Key (for OAuth)', + description: `Secret key used in the OAuth flow. [Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#generate-a-client_secret)`, type: 'string', isSecret: true, }, - }, - validationSchema: object().shape( - { - EXTERNAL_APPLE_ENABLED: boolean().required(), - EXTERNAL_APPLE_SECRET: string().when(['EXTERNAL_APPLE_ENABLED'], { - is: true, - then: (schema) => schema.required('Secret key is required'), - otherwise: (schema) => schema, - }), - EXTERNAL_APPLE_CLIENT_ID: string().when( - ['EXTERNAL_APPLE_ENABLED', 'EXTERNAL_IOS_BUNDLE_ID'], - { - is: (EXTERNAL_APPLE_ENABLED: boolean, EXTERNAL_IOS_BUNDLE_ID: string) => { - return EXTERNAL_APPLE_ENABLED && !EXTERNAL_IOS_BUNDLE_ID - }, - then: (schema) => schema.required('Either the Services ID or iOS Bundle ID is required'), - otherwise: (schema) => schema, - } - ), - EXTERNAL_IOS_BUNDLE_ID: string().when( - ['EXTERNAL_APPLE_ENABLED', 'EXTERNAL_APPLE_CLIENT_ID'], - { - is: (EXTERNAL_APPLE_ENABLED: boolean, EXTERNAL_APPLE_CLIENT_ID: string) => { - return EXTERNAL_APPLE_ENABLED && !EXTERNAL_APPLE_CLIENT_ID - }, - then: (schema) => schema.required('Either the Services ID or iOS Bundle ID is required'), - otherwise: (schema) => schema, - } - ), + EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS: { + title: 'Authorized Client IDs (iOS, macOS, watchOS, tvOS bundle IDs or service IDs)', + description: `Comma separated list of allowed Apple app bundle IDs for native sign in, or service IDs for Sign in with Apple JS. [Learn more](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js)`, + type: 'string', }, - // this is necessary for the "either or" validation on EXTERNAL_APPLE_CLIENT_ID and EXTERNAL_IOS_BUNDLE_ID - [['EXTERNAL_APPLE_CLIENT_ID', 'EXTERNAL_IOS_BUNDLE_ID']] - ), + }, + validationSchema: object().shape({ + EXTERNAL_APPLE_ENABLED: boolean().required(), + EXTERNAL_APPLE_SECRET: string() + .when(['EXTERNAL_APPLE_ENABLED', 'EXTERNAL_APPLE_CLIENT_ID'], { + is: (EXTERNAL_APPLE_ENABLED: boolean, EXTERNAL_APPLE_CLIENT_ID: string) => { + return EXTERNAL_APPLE_ENABLED && !!EXTERNAL_APPLE_CLIENT_ID + }, + then: (schema) => + schema + .required('Secret key is required when using the OAuth flow.') + .matches(/^[a-z0-9_-]+([.][a-z0-9_-]+){2}$/i, 'Secret key should be a JWT.') + .test({ + message: 'Secret key is not a correctly generated JWT.', + test: (value?: string): boolean => { + if (!value) { + return true + } + try { + const parts = value.split('.').map((value) => parseBase64URL(value)) + const header = JSON.parse(parts[0]) + const body = JSON.parse(parts[1]) + return ( + typeof header === 'object' && + typeof body === 'object' && + header && + body && + header.typ === 'JWT' && + header.alg === 'ES256' && + body.aud === 'https://appleid.apple.com' + ) + } catch (e: any) { + console.log(e) + return false + } + + return true + }, + }) + .test({ + message: 'Secret key expires in less than 7 days!', + test: (value?: string) => { + if (!value) { + return true + } + try { + const parts = value.split('.').map((value) => parseBase64URL(value)) + const body = JSON.parse(parts[1]) + return Date.now() > body.exp - 7 * 24 * 60 * 60 * 1000 + } catch (e: any) { + console.log(e) + return false + } + + return true + }, + }), + }) + .when( + [ + 'EXTERNAL_APPLE_ENABLED', + 'EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS', + 'EXTERNAL_APPLE_CLIENT_ID', + ], + { + is: ( + EXTERNAL_APPLE_ENABLED: boolean, + EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS: string, + EXTERNAL_APPLE_CLIENT_ID: string + ) => { + return ( + EXTERNAL_APPLE_ENABLED && + !!EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS && + !EXTERNAL_APPLE_CLIENT_ID + ) + }, + then: (schema) => + schema.matches( + /^$/, + 'Secret Key should only be set if Service ID for OAuth is provided.' + ), + } + ), + EXTERNAL_APPLE_CLIENT_ID: string().matches( + /^[a-z0-9.-]+$/i, + 'Invalid characters. Apple recommends a reverse-domain name style string (e.g. com.example.app).' + ), + EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS: string() + .matches( + /^([.a-z0-9-]+(,\s*[.a-z0-9-]+)*,*\s*)?$/i, + 'Invalid characters. Apple recommends a reverse-domain name style string (e.g. com.example.app). You must only use explicit bundle IDs, asterisks (*) are not allowed.' + ) + .when(['EXTERNAL_APPLE_ENABLED', 'EXTERNAL_APPLE_CLIENT_ID'], { + is: (EXTERNAL_APPLE_ENABLED: boolean, EXTERNAL_APPLE_CLIENT_ID: string) => { + return EXTERNAL_APPLE_ENABLED && !EXTERNAL_APPLE_CLIENT_ID + }, + then: (schema) => + schema.required( + 'At least one Authorized Client ID is required when not using the OAuth flow.' + ), + }), + }), misc: { iconKey: 'apple-icon', requiresRedirect: true, - helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console. + helper: `Register this callback URL when using Sign in with Apple on the web in the Apple Developer Center. [Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#configure-your-services-id)`, alert: { - title: `Apple secrets will self expire every 6 months`, - description: `You will need to regenerate before the 6 months elapses otherwise your users using Apple Login will no longer be able to log back in.`, + title: `Apple OAuth secret keys expire every 6 months`, + description: `A new secret should be generated every 6 months, otherwise users on the web will not be able to sign in.`, }, }, } @@ -630,37 +693,94 @@ const EXTERNAL_PROVIDER_GOOGLE = { title: 'Google', properties: { EXTERNAL_GOOGLE_ENABLED: { - title: 'Google enabled', + title: 'Enable Sign in with Google', + description: + 'Enables Sign in with Google on the web using OAuth or One Tap, or in Android apps or Chrome extensions.', type: 'boolean', }, - // [TODO] Update docs EXTERNAL_GOOGLE_CLIENT_ID: { - title: 'Client ID', + title: 'Client ID (for OAuth)', + description: 'Client ID to use with the OAuth flow on the web.', type: 'string', }, - // [TODO] Update docs EXTERNAL_GOOGLE_SECRET: { - title: 'Client Secret', + title: 'Client Secret (for OAuth)', + description: 'Client Secret to use with the OAuth flow on the web.', type: 'string', isSecret: true, }, + EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS: { + title: 'Authorized Client IDs (for Android, One Tap, and Chrome extensions)', + description: + 'Comma separated list of client IDs of Android apps, One Tap or Chrome extensions that are allowed to log in to your project.', + type: 'string', + }, }, validationSchema: object().shape({ EXTERNAL_GOOGLE_ENABLED: boolean().required(), - EXTERNAL_GOOGLE_CLIENT_ID: string().when('EXTERNAL_GOOGLE_ENABLED', { - is: true, - then: (schema) => schema.required('Client ID is required'), - otherwise: (schema) => schema, - }), - EXTERNAL_GOOGLE_SECRET: string().when('EXTERNAL_GOOGLE_ENABLED', { - is: true, - then: (schema) => schema.required('Client Secret is required'), - otherwise: (schema) => schema, - }), + EXTERNAL_GOOGLE_SECRET: string() + .when(['EXTERNAL_GOOGLE_ENABLED', 'EXTERNAL_GOOGLE_CLIENT_ID'], { + is: (EXTERNAL_GOOGLE_ENABLED: boolean, EXTERNAL_GOOGLE_CLIENT_ID: string) => { + return EXTERNAL_GOOGLE_ENABLED && !!EXTERNAL_GOOGLE_CLIENT_ID + }, + then: (schema) => + schema + .matches( + /^[a-z0-9.\/_-]*$/i, + 'Invalid characters. Google OAuth Client Secrets usually contain letters, numbers, dots, dashes and underscores.' + ) + .required('Client Secret is required when using the OAuth flow.'), + }) + .when( + [ + 'EXTERNAL_GOOGLE_ENABLED', + 'EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS', + 'EXTERNAL_GOOGLE_CLIENT_ID', + ], + { + is: ( + EXTERNAL_GOOGLE_ENABLED: boolean, + EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS: string, + EXTERNAL_GOOGLE_CLIENT_ID: string + ) => { + return ( + EXTERNAL_GOOGLE_ENABLED && + !!EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS && + !EXTERNAL_GOOGLE_CLIENT_ID + ) + }, + then: (schema) => + schema.matches( + /^$/, + 'Client Secret should only be set when Client ID for OAuth is set.' + ), + } + ), + EXTERNAL_GOOGLE_CLIENT_ID: string().matches( + /^([a-z0-9-]+([.][a-z0-9-]+)+)?$/i, + 'Invalid characters. Google OAuth Client IDs are usually a domain-name (e.g. 01234567890-abcdefghijklmnopqrstuvwxyz012345.apps.googleusercontent.com).' + ), + EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS: string() + .matches( + /^([a-z0-9-]+([.][a-z0-9-]+)*(,\s*[a-z0-9-]+([.][a-z0-9-]+)*)*,*\s*)?$/i, + 'Invalid characters. Google Client IDs are usually a domain-name style string (e.g. com.example.com.app or *.apps.googleusercontent.com).' + ) + + .when(['EXTERNAL_GOOGLE_ENABLED', 'EXTERNAL_GOOGLE_CLIENT_ID'], { + is: (EXTERNAL_GOOGLE_ENABLED: boolean, EXTERNAL_GOOGLE_CLIENT_ID: string) => { + return EXTERNAL_GOOGLE_ENABLED && !EXTERNAL_GOOGLE_CLIENT_ID + }, + then: (schema) => + schema.required( + 'At least one Authorized Client ID is required when not using the OAuth flow.' + ), + }), }), misc: { iconKey: 'google-icon', requiresRedirect: true, + helper: `Register this callback URL when using Sign-in with Google on the web using OAuth. + [Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#configure-your-services-id)`, }, } diff --git a/studio/tests/pages/projects/billing/Infrastructure.test.tsx b/studio/tests/pages/projects/billing/Infrastructure.test.tsx new file mode 100644 index 00000000000..f28fdb3522d --- /dev/null +++ b/studio/tests/pages/projects/billing/Infrastructure.test.tsx @@ -0,0 +1,24 @@ +import { get } from 'lib/common/fetch' +import { render } from '../../../helpers' +import { screen } from '@testing-library/react' +import Infrastructure from 'components/interfaces/BillingV2/Usage/Infrastructure' +beforeEach(() => { + // reset mocks between tests + ;(get as jest.Mock).mockReset() + ;(get as jest.Mock).mockImplementation(async (_url: string) => { + return [{ result: [] }] + }) +}) + +test(`renders category attributes static elements`, async () => { + render( + + ) + // renders usage categories info + await screen.findByText('Max CPU usage of your server') +})