Address conflicts

This commit is contained in:
Joshen Lim committed 2023-06-28 16:53:51 +08:00
commit c8edaae3e7
72 files changed
+2301 -389

No files matched your search

+10 -10
View File
@@ -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
@@ -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.
<SocialProviderSetup provider="Apple" />
### 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 `<project-id>.supabase.co` while the redirect URL is `https://<project-id>.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).
<Admonition>
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
<AppleSecretGenerator />
## Add your OAuth credentials to Supabase
## Using native sign in
<SocialProviderSettingsSupabase provider="Apple" />
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<AuthResponse> 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: '<identity token received from the OS>',
})
```
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: '<nonce used in AppleID.auth.init>',
})
}
```
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: '<value used in appleid-signin-nonce meta tag>',
})
})
```
## 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 }) => <Layout meta={meta} children={children} />
@@ -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
<SocialProviderSetup provider="Google" />
## 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
<SocialProviderSettingsSupabase provider="Google" />
## 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://<project-id>.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 `<project-id>.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: '<identity token received from the OS>',
access_token: '<access token received from the OS>'
})
````
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": "<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
<div
id="g_id_onload"
data-client_id="<client ID>"
data-context="signin"
data-ux_mode="popup"
data-callback="handleSignInWithGoogle"
data-nonce=""
data-auto_select="true"
data-itp_support="true"
></div>
<div
class="g_id_signin"
data-type="standard"
data-shape="pill"
data-theme="outline"
data-text="signin_with"
data-size="large"
data-logo_alignment="left"
></div>
```
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 }) => <Layout meta={meta} children={children} />
@@ -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
+1 -1
View File
@@ -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",
@@ -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:
@@ -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.
<Admonition type="caution">
Do not expose the `service_role` token on the client because the role is authorized to bypass
row-level security.
</Admonition>
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
@@ -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
@@ -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'
---
@@ -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'
---
@@ -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'
---
@@ -10,7 +10,6 @@ image: toadli-og.jpg
thumb: toadli-website.jpg
tags:
- supabase
- case-study
date: '03-08-2021'
---
@@ -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
---
@@ -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
---
@@ -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
@@ -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
@@ -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
@@ -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
---
@@ -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: '<id token>',
})
```
For Sign in with Google:
```tsx
await supabase.auth.signInWithIdToken({
provider: 'google',
token: '<id 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: '<client ID>');
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)
+8 -7
View File
@@ -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.
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable">
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.
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable">
We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for
all the ways we need use it, Supabase has been just as performant - if not more performant - than
the other vector databases.
</Quote>
## Tech stack
Mendable's tech stack includes React, Next.js, Express, Vercel, and Supabase.
+1 -1
View File
@@ -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
}),
},
+2 -2
View File
@@ -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)
})
})
+17 -12
View File
@@ -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,
+2 -2
View File
@@ -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
})
)
+2 -2
View File
@@ -110,7 +110,7 @@ function BlogPostPage(props: any) {
<div className="space-y-8 py-8 lg:py-0">
<div>
<div className="flex flex-wrap gap-2">
{props.blog.tags.map((tag: string) => {
{props.blog.tags?.map((tag: string) => {
return (
<a href={`/blog/tags/${tag}`} key={`category-badge-${tag}`}>
<Badge>{tag}</Badge>
@@ -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
}),
},
+1 -1
View File
@@ -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
})
)
+1 -1
View File
@@ -94,7 +94,7 @@ const OpenSource = () => {
</div>
<div className="grid lg:grid-cols-2 gap-4">
{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) => (
<div className="" key={idx}>
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

+1 -1
View File
@@ -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}
@@ -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 ?? ''
)
+1 -1
View File
@@ -43,7 +43,7 @@
- [x] পাবলিক বেটা: বেশিরভাগ নন-এন্টারপ্রাইজ ব্যবহারের ক্ষেত্রে যথেষ্ট স্থিতিশীল
- [ ] পাবলিক: প্রোডাকশন রেডি
আমরা বর্তমানে পাবলিক বেটাতে আছি। বড় আপডেটের বিজ্ঞপ্তি পেতে এই রিপুর "রিলিজ" দেখুন।
আমরা বর্তমানে পাবলিক বিটাতে আছি। বড় আপডেটের বিজ্ঞপ্তি পেতে এই রেপোর "রিলিজ" দেখুন।
<kbd><img src="https://raw.githubusercontent.com/supabase/supabase/d5f7f413ab356dc1a92075cb3cee4e40a957d5b1/web/static/watch-repo.gif" alt="এই রিপু দেখুন"/></kbd>
+2 -2
View File
@@ -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'
}
+2 -2
View File
@@ -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'
}
@@ -151,19 +151,21 @@ const ProviderForm = ({ provider }: ProviderFormProps) => {
)}
{provider.misc.requiresRedirect && (
<>
<ReactMarkdown className="text-xs text-scale-900">
{provider.misc.helper}
</ReactMarkdown>
<Input
copy
readOnly
disabled
label="Redirect URL"
label="Callback URL (for OAuth)"
value={
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain?.hostname}/auth/v1/callback`
: `https://${selectedProject?.ref}.supabase.co/auth/v1/callback`
}
descriptionText={
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{provider.misc.helper}
</ReactMarkdown>
}
/>
</>
)}
@@ -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 = ({
</div>
)}
{attribute.key === 'cpu_usage' && (
{attribute.key === 'max_cpu_usage' && (
<p className="text-sm text-scale-1000">
Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores.
</p>
@@ -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: [
{
@@ -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
@@ -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:'
}
@@ -0,0 +1,33 @@
export interface AuthorizeRequesterDetailsProps {
icon: string | null
name: string
domain: string
}
const AuthorizeRequesterDetails = ({ icon, name, domain }: AuthorizeRequesterDetailsProps) => {
return (
<div className="flex space-x-4">
<div>
<div className="flex items-center">
<div
className="w-14 h-14 md:w-16 md:h-16 bg-center bg-no-repeat bg-cover flex items-center justify-center rounded-md border border-scale-600"
style={{
backgroundImage: icon !== null ? `url('${icon}')` : 'none',
}}
>
{icon === null && <p className="text-scale-1000 text-lg">{name[0]}</p>}
</div>
</div>
</div>
<p className="text-sm text-scale-1100">
{name} ({domain}) is requesting API access to an organization. The application will be able
to{' '}
<span className="text-amber-1200">
read and write the organization's settings and all of its projects.
</span>
</p>
</div>
)
}
export default AuthorizeRequesterDetails
@@ -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 (
<Table.tr>
<Table.td>
<div
className="w-[30px] h-[30px] rounded-full bg-no-repeat bg-cover bg-center border border-scale-600 flex items-center justify-center"
style={{ backgroundImage: app.icon ? `url('${app.icon}')` : 'none' }}
>
{!!app.icon ? '' : `${app.name[0]}`}
</div>
</Table.td>
<Table.td>{app.name}</Table.td>
<Table.td>
<span className="font-mono truncate">{app.id}</span>
<Button
type="default"
icon={
isCopied ? <IconCheck className="text-brand-900" strokeWidth={3} /> : <IconClipboard />
}
className="ml-2 px-1"
onClick={() => {
copyToClipboard(app.id)
setIsCopied(true)
setTimeout(() => {
setIsCopied(false)
}, 3000)
}}
/>
</Table.td>
<Table.td>{dayjs(app.authorized_at).format('DD/MM/YYYY, HH:mm:ss')}</Table.td>
<Table.td align="right">
<Button
type="default"
icon={<IconTrash />}
className="px-1"
onClick={() => onSelectRevoke()}
/>
</Table.td>
</Table.tr>
)
}
export default AuthorizedAppRow
@@ -0,0 +1,72 @@
import { useParams } from 'common'
import { useOAuthAppDeleteMutation } from 'data/oauth/oauth-app-delete-mutation'
import { OAuthApp } from 'data/oauth/oauth-apps-query'
import { useStore } from 'hooks'
import { Alert, IconLock, Modal } from 'ui'
export interface DeleteAppModalProps {
selectedApp?: OAuthApp
onClose: () => void
}
const DeleteAppModal = ({ selectedApp, onClose }: DeleteAppModalProps) => {
const { ui } = useStore()
const { slug } = useParams()
const { mutateAsync: deleteOAuthApp, isLoading: isDeleting } = useOAuthAppDeleteMutation()
const onConfirmDelete = async () => {
if (!slug) return console.error('Slug is required')
if (!selectedApp?.id) return console.error('App ID is required')
try {
await deleteOAuthApp({ slug, id: selectedApp?.id })
ui.setNotification({
category: 'success',
message: `Successfully deleted the app "${selectedApp?.name}"`,
})
onClose()
} catch (error) {
ui.setNotification({
category: 'error',
message: `Failed to delete OAuth app: ${(error as any).message}`,
})
}
}
return (
<Modal
size="medium"
alignFooter="right"
header={`Confirm to delete ${selectedApp?.name}`}
visible={selectedApp !== undefined}
loading={isDeleting}
onCancel={onClose}
onConfirm={onConfirmDelete}
>
<Modal.Content>
<div className="py-4">
<Alert withIcon variant="warning" title="This action cannot be undone">
Deleting {selectedApp?.name} will invalidate any access tokens from this application
that were authorized by users.
</Alert>
<ul className="mt-4 space-y-5">
<li className="flex gap-3 text-sm">
<IconLock w={14} className="flex-shrink-0" />
<div>
<strong>Before you remove this application, consider:</strong>
<ul className="space-y-2 mt-2">
<li className="list-disc ml-4">
No users are currently using this application. It will no longer be available
for use after deletion.
</li>
</ul>
</div>
</li>
</ul>
</div>
</Modal.Content>
</Modal>
)
}
export default DeleteAppModal
@@ -0,0 +1,79 @@
import Table from 'components/to-be-cleaned/Table'
import { OAuthApp } from 'data/oauth/oauth-apps-query'
import dayjs from 'dayjs'
import { copyToClipboard } from 'lib/helpers'
import { useState } from 'react'
import {
Button,
Dropdown,
IconCheck,
IconClipboard,
IconEdit,
IconMoreVertical,
IconTrash,
} from 'ui'
export interface OAuthAppRowProps {
app: OAuthApp
onSelectEdit: () => void
onSelectDelete: () => void
}
const OAuthAppRow = ({ app, onSelectEdit, onSelectDelete }: OAuthAppRowProps) => {
const [isCopied, setIsCopied] = useState(false)
return (
<Table.tr>
<Table.td>
<div
className="w-[30px] h-[30px] rounded-full bg-no-repeat bg-cover bg-center border border-scale-600 flex items-center justify-center"
style={{ backgroundImage: app.icon ? `url('${app.icon}')` : 'none' }}
>
{!!app.icon ? '' : `${app.name[0]}`}
</div>
</Table.td>
<Table.td>{app.name}</Table.td>
<Table.td>
<span className="font-mono truncate">{app.client_id}</span>
<Button
type="default"
icon={
isCopied ? <IconCheck className="text-brand-900" strokeWidth={3} /> : <IconClipboard />
}
className="ml-2 px-1"
onClick={() => {
copyToClipboard(app.client_id)
setIsCopied(true)
setTimeout(() => {
setIsCopied(false)
}, 3000)
}}
/>
</Table.td>
<Table.td>
<span className="font-mono">{app.client_secret_alias}...</span>
</Table.td>
<Table.td>{dayjs(app.created_at).format('DD/MM/YYYY, HH:mm:ss')}</Table.td>
<Table.td align="right">
<Dropdown
size="tiny"
align="end"
side="bottom"
overlay={[
<Dropdown.Item key="edit" icon={<IconEdit />} onClick={() => onSelectEdit()}>
Edit app
</Dropdown.Item>,
<Dropdown.Separator key="separator" />,
<Dropdown.Item key="delete" icon={<IconTrash />} onClick={() => onSelectDelete()}>
Delete app
</Dropdown.Item>,
]}
>
<Button type="default" icon={<IconMoreVertical />} className="px-1" />
</Dropdown>
</Table.td>
</Table.tr>
)
}
export default OAuthAppRow
@@ -0,0 +1,230 @@
import { useState } from 'react'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { OAuthAppCreateResponse } from 'data/oauth/oauth-app-create-mutation'
import { OAuthApp, useOAuthAppsQuery } from 'data/oauth/oauth-apps-query'
import { Alert, Button, IconX, Input } from 'ui'
import DeleteAppModal from './DeleteAppModal'
import OAuthAppRow from './OAuthAppRow'
import PublishAppModal from './PublishAppSidePanel'
import { AuthorizedApp, useAuthorizedAppsQuery } from 'data/oauth/authorized-apps-query'
import AuthorizedAppRow from './AuthorizedAppRow'
import RevokeAppModal from './RevokeAppModal'
// [Joshen] Note on nav UX
// Kang Ming mentioned that it might be better to split Published Apps and Authorized Apps into 2 separate tabs
// to prevent any confusion (case study: Github). Authorized apps could be in the "integrations" tab, but let's
// check in again after we wrap up Vercel integration
const OAuthApps = () => {
const { slug } = useParams()
const [createdApp, setCreatedApp] = useState<OAuthAppCreateResponse>()
const [showPublishModal, setShowPublishModal] = useState(false)
const [selectedAppToUpdate, setSelectedAppToUpdate] = useState<OAuthApp>()
const [selectedAppToDelete, setSelectedAppToDelete] = useState<OAuthApp>()
const [selectedAppToRevoke, setSelectedAppToRevoke] = useState<AuthorizedApp>()
const {
data: publishedApps,
isLoading: isLoadingPublishedApps,
isSuccess: isSuccessPublishedApps,
isError: isErrorPublishedApps,
} = useOAuthAppsQuery({ slug })
const sortedPublishedApps = publishedApps?.sort((a, b) => {
return Number(new Date(a.created_at)) - Number(new Date(b.created_at))
})
const {
data: authorizedApps,
isLoading: isLoadingAuthorizedApps,
isSuccess: isSuccessAuthorizedApps,
isError: isErrorAuthorizedApps,
} = useAuthorizedAppsQuery({ slug })
const sortedAuthorizedApps = authorizedApps?.sort((a, b) => {
return Number(new Date(a.authorized_at)) - Number(new Date(b.authorized_at))
})
return (
<>
<div className="container my-4 max-w-5xl space-y-8">
<div>
<div className="flex items-center justify-between">
<div>
<p>Published Apps</p>
<p className="text-scale-1000 text-sm">
Build integrations that extend Supabase's functionality
</p>
</div>
<Button type="primary" onClick={() => setShowPublishModal(true)}>
Add application
</Button>
</div>
<div className="mt-4">
{isLoadingPublishedApps && (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)}
{isErrorPublishedApps && (
<AlertError subject="Unable to retrieve published OAuth apps" />
)}
{createdApp !== undefined && (
<Alert withIcon variant="success" title="Successfully published a new application!">
<div className="absolute top-4 right-4">
<Button
type="text"
icon={<IconX size={18} />}
className="px-1"
onClick={() => setCreatedApp(undefined)}
/>
</div>
<div className="w-full space-y-4">
<p className="text-sm">
Ensure that you store the client secret securely - you will not be able to see
it again.
</p>
<div className="space-y-4">
<Input
copy
readOnly
size="small"
// layout="horizontal"
label="Client ID"
className="max-w-xl input-mono"
value={createdApp.client_id}
onChange={() => {}}
/>
<Input
copy
readOnly
size="small"
// layout="horizontal"
label="Client secret"
className="max-w-xl input-mono"
value={createdApp.client_secret}
onChange={() => {}}
/>
</div>
</div>
</Alert>
)}
{isSuccessPublishedApps && (
<>
{(publishedApps?.length ?? 0) === 0 ? (
<div className="bg-scale-100 dark:bg-scale-300 border rounded p-4 flex items-center justify-between">
<p className="prose text-sm">You do not have any published applications yet</p>
</div>
) : (
<Table
className="mt-4"
head={[
<Table.th key="icon" className="w-[30px]"></Table.th>,
<Table.th key="name">Name</Table.th>,
<Table.th key="client-id">Client ID</Table.th>,
<Table.th key="client-secret">Client Secret</Table.th>,
<Table.th key="client-secret">Created at</Table.th>,
<Table.th key="delete-action"></Table.th>,
]}
body={
sortedPublishedApps?.map((app) => (
<OAuthAppRow
key={app.id}
app={app}
onSelectEdit={() => {
setShowPublishModal(true)
setSelectedAppToUpdate(app)
}}
onSelectDelete={() => setSelectedAppToDelete(app)}
/>
)) ?? []
}
/>
)}
</>
)}
</div>
</div>
<div>
<p>Authorized Apps</p>
<p className="text-scale-1000 text-sm">
Applications that have access to your organization's settings and projects
</p>
<div className="mt-4">
{isLoadingAuthorizedApps && (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)}
{isErrorAuthorizedApps && <AlertError subject="Unable to retrieve authorized apps" />}
{isSuccessAuthorizedApps && (
<>
{(authorizedApps.length ?? 0) === 0 ? (
<div className="bg-scale-100 dark:bg-scale-300 border rounded p-4 flex items-center justify-between">
<p className="prose text-sm">You do not have any authorized applications yet</p>
</div>
) : (
<Table
className="mt-4"
head={[
<Table.th key="icon" className="w-[30px]"></Table.th>,
<Table.th key="name">Name</Table.th>,
<Table.th key="id">ID</Table.th>,
<Table.th key="client-secret">Authorized at</Table.th>,
<Table.th key="delete-action"></Table.th>,
]}
body={
sortedAuthorizedApps?.map((app) => (
<AuthorizedAppRow
key={app.id}
app={app}
onSelectRevoke={() => setSelectedAppToRevoke(app)}
/>
)) ?? []
}
/>
)}
</>
)}
</div>
</div>
</div>
<PublishAppModal
visible={showPublishModal}
selectedApp={selectedAppToUpdate}
onClose={() => {
setSelectedAppToUpdate(undefined)
setShowPublishModal(false)
}}
onCreateSuccess={setCreatedApp}
/>
<DeleteAppModal
selectedApp={selectedAppToDelete}
onClose={() => setSelectedAppToDelete(undefined)}
/>
<RevokeAppModal
selectedApp={selectedAppToRevoke}
onClose={() => setSelectedAppToRevoke(undefined)}
/>
</>
)
}
export default OAuthApps
@@ -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<any>()
const { mutateAsync: createOAuthApp } = useOAuthAppCreateMutation()
const { mutateAsync: updateOAuthApp } = useOAuthAppUpdateMutation()
const [showPreview, setShowPreview] = useState(false)
const [iconFile, setIconFile] = useState<File>()
const [iconUrl, setIconUrl] = useState<string>()
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<HTMLInputElement>) => {
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 (
<>
<SidePanel
hideFooter
size="large"
visible={visible}
header={
selectedApp !== undefined ? 'Update OAuth application' : 'Publish a new OAuth application'
}
onCancel={() => onClose()}
>
<Form
validateOnBlur
className="h-full"
initialValues={{ name: '', website: '' }}
validate={validate}
onSubmit={onSubmit}
>
{({
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 (
<>
<div className="h-full flex flex-col">
<div className="flex-grow">
<SidePanel.Content>
<div className="py-4 flex items-start justify-between gap-10">
<div className="space-y-4 w-full">
<Input
id="name"
label="Application name"
descriptionText={selectedApp?.id && `ID: ${selectedApp.id}`}
/>
<Input
id="website"
label="Website URL"
placeholder="https://my-website.com"
/>
</div>
<div>
{iconUrl !== undefined ? (
<div
className={clsx(
'shadow transition group relative',
'bg-center bg-cover bg-no-repeat',
'mt-4 mr-4 space-y-2 rounded-full h-[120px] w-[120px] flex flex-col items-center justify-center'
)}
style={{
backgroundImage: iconUrl ? `url("${iconUrl}")` : 'none',
}}
>
<div className="absolute bottom-1 right-1">
<Dropdown
size="tiny"
align="end"
side="bottom"
overlay={[
<Dropdown.Item
key="upload"
onClick={() => {
if (uploadButtonRef.current)
(uploadButtonRef.current as any).click()
}}
>
Upload image
</Dropdown.Item>,
<Dropdown.Item
key="remove"
onClick={() => {
setIconFile(undefined)
setIconUrl(undefined)
}}
>
Remove image
</Dropdown.Item>,
]}
>
<Button type="default" icon={<IconEdit />} className="px-1" />
</Dropdown>
</div>
</div>
) : (
<div
className={clsx(
'border border-scale-700 transition opacity-75 hover:opacity-100',
'mt-4 mr-4 space-y-2 rounded-full h-[120px] w-[120px] flex flex-col items-center justify-center cursor-pointer'
)}
onClick={() => {
if (uploadButtonRef.current)
(uploadButtonRef.current as any).click()
}}
>
<IconUpload size={18} strokeWidth={1.5} className="text-scale-1200" />
<p className="text-xs text-scale-1100">Upload logo</p>
</div>
)}
<input
multiple
type="file"
ref={uploadButtonRef}
className="hidden"
accept="image/png, image/jpeg"
onChange={onFileUpload}
/>
</div>
</div>
</SidePanel.Content>
<SidePanel.Separator />
<SidePanel.Content className="py-4">
<div className="mb-2 flex items-center justify-between">
<div>
<p className="prose text-sm">Authorization callback URLs</p>
<p className="text-sm text-scale-1000">
All URLs must use HTTPS, except for localhost
</p>
</div>
<Button onClick={() => setUrls(urls.concat({ id: uuidv4(), value: '' }))}>
Add URL
</Button>
</div>
<div className="space-y-2 pb-2">
{urls.map((url) => (
<Input
key={url.id}
value={url.value}
onChange={(event) => onUpdateUrl(url.id, event.target.value)}
placeholder="e.g https://my-website.com"
actions={[
urls.length > 1 ? (
<Button
key="remove-url"
type="default"
onClick={() => removeUrl(url.id)}
>
Remove
</Button>
) : null,
]}
/>
))}
{errors.urls && <p className="text-red-900 text-sm">{errors.urls}</p>}
</div>
</SidePanel.Content>
<SidePanel.Separator />
</div>
<SidePanel.Separator />
<SidePanel.Content>
<div className="pt-2 pb-3 flex items-center justify-between">
<Button
type="default"
onClick={() => setShowPreview(true)}
disabled={values.name.length === 0 || values.website.length === 0}
>
Preview consent for users
</Button>
<div className="flex items-center space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
Cancel
</Button>
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting}>
Confirm
</Button>
</div>
</div>
</SidePanel.Content>
</div>
<Modal
hideFooter
className="!w-[500px]"
visible={showPreview}
onCancel={() => setShowPreview(false)}
>
<Modal.Content>
<div className="pt-4 pb-2 px-2 flex items-center justify-between">
<p>Authorize API access for {values.name}</p>
<Badge color="green">Preview</Badge>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="px-2 py-4">
<AuthorizeRequesterDetails
icon={iconUrl || null}
name={values.name}
domain={values.website}
/>
<div className="pt-4 space-y-2">
<p className="prose text-sm">
Select an organization to grant API access to
</p>
<div className="border border-scale-600 text-scale-1000 rounded px-4 py-2 text-sm bg-scale-400">
Organizations that you have access to will be listed here
</div>
</div>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="pt-2 pb-3 flex items-center justify-between">
<p className="prose text-xs">
This is what your users will see when authorizing with your app
</p>
<Button type="default" onClick={() => setShowPreview(false)}>
Close
</Button>
</div>
</Modal.Content>
</Modal>
</>
)
}}
</Form>
</SidePanel>
</>
)
}
export default PublishAppModal
@@ -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 (
<Modal
size="medium"
alignFooter="right"
header={`Confirm to revoke ${selectedApp?.name}`}
visible={selectedApp !== undefined}
loading={isDeleting}
onCancel={onClose}
onConfirm={onConfirmDelete}
>
<Modal.Content>
<div className="py-4">
<Alert withIcon variant="warning" title="This action cannot be undone">
{selectedApp?.name} application will no longer have access to your organization's
settings and projects.
</Alert>
<ul className="mt-4 space-y-5">
<li className="flex gap-3 text-sm">
<IconLock w={14} className="flex-shrink-0" />
<div>
<strong>Before you remove this app, consider:</strong>
<ul className="space-y-2 mt-2">
<li className="list-disc ml-4">
No users are currently using this application. The application will no longer
have access to your organization after being revoked.
</li>
</ul>
</div>
</li>
</ul>
</div>
</Modal.Content>
</Modal>
)
}
export default RevokeAppModal
@@ -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 }
@@ -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) {
@@ -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({
@@ -83,19 +83,20 @@ const SideBarContent = observer(() => {
<div className="mt-6">
<Menu type="pills">
{IS_PLATFORM && (
<div className="my-4 mx-3 space-y-1 px-3">
<div className="flex items-center">
<div className="my-4 mx-3 space-y-2 px-3">
<div className="flex items-center mb-3 w-full justify-center min-w-[210px] ">
<Dropdown
align="start"
side="bottom"
sideOffset={3}
className="max-w-[210px] this-class-is-no-bueno"
className="max-w-[210px]"
overlay={[
<Dropdown.Item key="new-blank-query" onClick={() => handleNewQuery()}>
<div className="space-y-1">
<p className="block text-scale-1200">New blank query</p>
<p className="block text-scale-1200 text-xs">New blank query</p>
</div>
</Dropdown.Item>,
<div key={'divider'} className="my-1 border-t border-scale-400" />,
showCmdkHelper ? (
<Dropdown.Item
key="new-ai-query"
@@ -105,7 +106,7 @@ const SideBarContent = observer(() => {
}}
>
<div className="space-y-1">
<p className="block text-scale-1200">New AI query</p>
<p className="block text-scale-1200 text-xs">New AI query</p>
</div>
</Dropdown.Item>
) : null,
@@ -114,7 +115,8 @@ const SideBarContent = observer(() => {
<Button
block
icon={<IconPlus />}
type="primary"
className="min-w-[208px]"
type="outline"
disabled={isLoading}
style={{ justifyContent: 'start' }}
>
+38
View File
@@ -0,0 +1,38 @@
import Link from 'next/link'
import { Alert, Button } from 'ui'
export interface AlertErrorProps {
ref?: string
subject?: string
}
// [Joshen] To standardize the language for all error UIs
const AlertError = ({ ref, subject }: AlertErrorProps) => {
const subjectString = subject?.replace(/ /g, '%20')
let href = `/support/new?category=dashboard_bug`
if (ref) href += `&ref=${ref}`
if (subjectString) href += `&subject=${subjectString}`
return (
<Alert
withIcon
variant="warning"
title={subject}
actions={[
<Link key="contact-support" href={href}>
<a>
<Button type="default" className="ml-4">
Contact support
</Button>
</a>
</Link>,
]}
>
Try refreshing your browser, but if the issue persists, please reach out to us via support.
</Alert>
)
}
export default AlertError
@@ -51,7 +51,6 @@ const DisplayApiSettings = () => {
Client Docs
</Button>
</a>
.
</p>
</div>
}
@@ -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'
@@ -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<ReturnType<typeof revokeAuthorizedApp>>
export const useAuthorizedAppRevokeMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<AuthorizedAppRevokeData, unknown, AuthorizedAppRevokeVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<AuthorizedAppRevokeData, unknown, AuthorizedAppRevokeVariables>(
(vars) => revokeAuthorizedApp(vars),
{
async onSuccess(data, variables, context) {
const { slug } = variables
await queryClient.invalidateQueries(oauthAppKeys.authorizedApps(slug))
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
@@ -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<ReturnType<typeof getAuthorizedApps>>
export type AuthorizedAppsError = ResponseError
export const useAuthorizedAppsQuery = <TData = AuthorizedAppsData>(
{ slug }: AuthorizedAppsVariables,
{
enabled = true,
...options
}: UseQueryOptions<AuthorizedAppsData, AuthorizedAppsError, TData> = {}
) =>
useQuery<AuthorizedAppsData, AuthorizedAppsError, TData>(
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])
}
+4
View File
@@ -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,
}
@@ -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<ReturnType<typeof createOAuthApp>>
export const useOAuthAppCreateMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<OAuthAppCreateData, unknown, OAuthAppCreateVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<OAuthAppCreateData, unknown, OAuthAppCreateVariables>(
(vars) => createOAuthApp(vars),
{
async onSuccess(data, variables, context) {
const { slug } = variables
await queryClient.invalidateQueries(oauthAppKeys.oauthApps(slug))
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
@@ -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<ReturnType<typeof deleteOAuthApp>>
export const useOAuthAppDeleteMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<OAuthAppDeleteData, unknown, OAuthAppDeleteVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<OAuthAppDeleteData, unknown, OAuthAppDeleteVariables>(
(vars) => deleteOAuthApp(vars),
{
async onSuccess(data, variables, context) {
const { slug } = variables
await queryClient.invalidateQueries(oauthAppKeys.oauthApps(slug))
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
@@ -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<ReturnType<typeof updateOAuthApp>>
export const useOAuthAppUpdateMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<OAuthAppUpdateData, unknown, OAuthAppUpdateVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<OAuthAppUpdateData, unknown, OAuthAppUpdateVariables>(
(vars) => updateOAuthApp(vars),
{
async onSuccess(data, variables, context) {
const { slug } = variables
await queryClient.invalidateQueries(oauthAppKeys.oauthApps(slug))
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
+59
View File
@@ -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<ReturnType<typeof getOAuthApps>>
export type OAuthAppsError = ResponseError
export const useOAuthAppsQuery = <TData = OAuthAppsData>(
{ slug }: OAuthAppsVariables,
{ enabled = true, ...options }: UseQueryOptions<OAuthAppsData, OAuthAppsError, TData> = {}
) =>
useQuery<OAuthAppsData, OAuthAppsError, TData>(
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])
}
+8 -2
View File
@@ -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,
},
+10
View File
@@ -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:'
}
+45
View File
@@ -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
}
+10 -27
View File
@@ -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 = () => {
>
<div className="w-full md:w-[500px] px-8 py-6 space-y-8">
{/* API Authorization requester details */}
<div className="flex space-x-4">
<div>
<div className="rounded-md border border-scale-600 p-2.5 flex items-center">
<div
className="w-8 h-8 md:w-10 md:h-10 bg-center bg-no-repeat bg-cover flex items-center justify-center"
style={{
backgroundImage: requester.icon !== null ? `url('${requester?.icon}')` : 'none',
}}
>
{requester?.icon === null && (
<p className="text-scale-1000 text-lg">{requester?.name[0]}</p>
)}
</div>
</div>
</div>
<p className="text-sm text-scale-1100">
{requester?.name} ({requester?.domain}) is requesting API access to an organization. The
application will be able to{' '}
<span className="text-amber-1200">
read and write the organization's settings and all of its projects.
</span>
</p>
</div>
<AuthorizeRequesterDetails
icon={requester.icon}
name={requester.name}
domain={requester.domain}
/>
{/* Expiry warning */}
{isExpired && (
+57
View File
@@ -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 ? (
<Loading />
) : (
<div className="p-4 pt-0">
<div className="space-y-3">
<section className="mt-4">
<h1 className="text-3xl">
{ui.selectedOrganization?.name ?? 'Organization'} settings
</h1>
</section>
<nav>
<Tabs
size="small"
type="underlined"
activeId="apps"
onChange={(id: any) => {
if (id !== 'apps') router.push(`/org/${slug}/${id}`)
}}
>
<Tabs.Panel id="general" label="General" />
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
<Tabs.Panel id="apps" label="OAuth Apps" />
</Tabs>
</nav>
</div>
<div className="mb-8">
<OAuthApps />
</div>
</div>
)}
</>
)
}
OrgOAuthApps.getLayout = (page) => <OrganizationLayout>{page}</OrganizationLayout>
export default observer(OrgOAuthApps)
+3 -1
View File
@@ -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 = () => {
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
{showOAuthApps && <Tabs.Panel id="apps" label="OAuth Apps" />}
</Tabs>
</nav>
</div>
+3 -1
View File
@@ -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 = () => {
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
{showOAuthApps && <Tabs.Panel id="apps" label="OAuth Apps" />}
</Tabs>
</nav>
</div>
+3 -1
View File
@@ -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 = () => {
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
{showOAuthApps && <Tabs.Panel id="apps" label="OAuth Apps" />}
</Tabs>
</nav>
</div>
+3 -1
View File
@@ -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 = () => {
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
{showOAuthApps && <Tabs.Panel id="apps" label="OAuth Apps" />}
</Tabs>
</nav>
</div>
@@ -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)`,
},
}
@@ -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(
<Infrastructure
projectRef={'someref'}
startDate={new Date().toISOString()}
endDate={new Date().toISOString()}
currentBillingCycleSelected={false}
/>
)
// renders usage categories info
await screen.findByText('Max CPU usage of your server')
})