mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 22:59:11 +03:00
Merge branch 'master' into chore/docs-warnings-cleanup
This commit is contained in:
commit
da199a7329
34 files changed
+585
-326
No files matched your search
@@ -48,7 +48,12 @@ const nextConfig = {
|
||||
basePath: '/docs',
|
||||
images: {
|
||||
dangerouslyAllowSVG: true,
|
||||
domains: ['avatars.githubusercontent.com', 'github.com', 'user-images.githubusercontent.com'],
|
||||
domains: [
|
||||
'avatars.githubusercontent.com',
|
||||
'github.com',
|
||||
'user-images.githubusercontent.com',
|
||||
'raw.githubusercontent.com',
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
mdxRs: true,
|
||||
|
||||
@@ -16,7 +16,7 @@ Self-hosting Supabase is free. If you wish to use our cloud-platform, we provide
|
||||
|
||||
### How do I host Supabase?
|
||||
|
||||
You can use the docker-compose script [here](https://github.com/supabase/supabase/tree/master/docker), and find detailed instructions [here](/docs/guides/hosting/overview).
|
||||
You can use the docker compose script [here](https://github.com/supabase/supabase/tree/master/docker), and find detailed instructions [here](/docs/guides/hosting/overview).
|
||||
|
||||
Supabase is an amalgamation of open source tools. Some of these tools are made by Supabase (like our [Realtime Server](https://github.com/supabase/realtime)), some we support directly (like [PostgREST](http://postgrest.org/en/v7.0.0/)), and some are third-party tools (like [KonSupabase is an amalgamation open sourceg](https://github.com/Kong/kong)).
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ cd supabase-authsignal-example
|
||||
|
||||
Create a `.env.local` file and enter the following values:
|
||||
|
||||
```
|
||||
```sh
|
||||
NEXT_PUBLIC_SUPABASE_URL=get-from-supabase-dashboard
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=get-from-supabase-dashboard
|
||||
AUTHSIGNAL_SECRET=get-from-authsignal-dashboard
|
||||
@@ -84,19 +84,19 @@ npm run dev
|
||||
|
||||
Install the Supabase client and Auth helpers for Next.js:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install @supabase/supabase-js @supabase/auth-helpers-nextjs
|
||||
```
|
||||
|
||||
Install the Authsignal Node.js client:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install @authsignal/node
|
||||
```
|
||||
|
||||
Finally install 2 packages to help encrypt and serialize session data in cookies:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install @hapi/iron cookie
|
||||
npm install --save-dev @types/cookie
|
||||
```
|
||||
@@ -105,18 +105,18 @@ npm install --save-dev @types/cookie
|
||||
|
||||
Add the following code to `/lib/authsignal.ts`:
|
||||
|
||||
```
|
||||
import { Authsignal } from "@authsignal/node";
|
||||
```ts
|
||||
import { Authsignal } from '@authsignal/node'
|
||||
|
||||
const secret = process.env.AUTHSIGNAL_SECRET;
|
||||
const secret = process.env.AUTHSIGNAL_SECRET
|
||||
|
||||
if (!secret) {
|
||||
throw new Error("AUTHSIGNAL_SECRET is undefined");
|
||||
throw new Error('AUTHSIGNAL_SECRET is undefined')
|
||||
}
|
||||
|
||||
const redirectUrl = "http://localhost:3000/api/callback";
|
||||
const redirectUrl = 'http://localhost:3000/api/callback'
|
||||
|
||||
export const authsignal = new Authsignal({ secret, redirectUrl });
|
||||
export const authsignal = new Authsignal({ secret, redirectUrl })
|
||||
```
|
||||
|
||||
The `redirectUrl` here is a Next.js API route which Authsignal will redirect back to after an MFA challenge. We'll implement this below.
|
||||
@@ -131,83 +131,72 @@ Next we will add some helper functions for managing cookies:
|
||||
|
||||
Add the following code to `/lib/cookies.ts`:
|
||||
|
||||
```
|
||||
import Iron from "@hapi/iron";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import { parse, serialize } from "cookie";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
```ts
|
||||
import Iron from '@hapi/iron'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
import { parse, serialize } from 'cookie'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function setTempCookie(session: Session, res: NextApiResponse) {
|
||||
const token = await Iron.seal(session, TEMP_TOKEN_SECRET, Iron.defaults);
|
||||
const token = await Iron.seal(session, TEMP_TOKEN_SECRET, Iron.defaults)
|
||||
|
||||
const cookie = serialize(TEMP_COOKIE, token, {
|
||||
maxAge: session.expires_in,
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
});
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
})
|
||||
|
||||
res.setHeader("Set-Cookie", cookie);
|
||||
res.setHeader('Set-Cookie', cookie)
|
||||
}
|
||||
|
||||
export async function getSessionFromTempCookie(
|
||||
req: NextApiRequest
|
||||
): Promise<Session | undefined> {
|
||||
const cookie = req.headers.cookie as string;
|
||||
export async function getSessionFromTempCookie(req: NextApiRequest): Promise<Session | undefined> {
|
||||
const cookie = req.headers.cookie as string
|
||||
|
||||
const cookies = parse(cookie ?? "");
|
||||
const cookies = parse(cookie ?? '')
|
||||
|
||||
const tempCookie = cookies[TEMP_COOKIE];
|
||||
const tempCookie = cookies[TEMP_COOKIE]
|
||||
|
||||
if (!tempCookie) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const session = await Iron.unseal(
|
||||
tempCookie,
|
||||
TEMP_TOKEN_SECRET,
|
||||
Iron.defaults
|
||||
);
|
||||
const session = await Iron.unseal(tempCookie, TEMP_TOKEN_SECRET, Iron.defaults)
|
||||
|
||||
return session;
|
||||
return session
|
||||
}
|
||||
|
||||
export function setAuthCookie(session: Session, res: NextApiResponse) {
|
||||
const { access_token, refresh_token, expires_in } = session;
|
||||
const { access_token, refresh_token, expires_in } = session
|
||||
|
||||
const authCookies = [
|
||||
{ name: ACCESS_TOKEN_COOKIE, value: access_token },
|
||||
refresh_token
|
||||
? { name: REFRESH_TOKEN_COOKIE, value: refresh_token }
|
||||
: undefined,
|
||||
refresh_token ? { name: REFRESH_TOKEN_COOKIE, value: refresh_token } : undefined,
|
||||
]
|
||||
.filter(isDefined)
|
||||
.map(({ name, value }) =>
|
||||
serialize(name, value, {
|
||||
maxAge: expires_in,
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
// Also clear the temp cookie
|
||||
const updatedCookies = [
|
||||
...authCookies,
|
||||
serialize(TEMP_COOKIE, "", { maxAge: -1, path: "/" }),
|
||||
];
|
||||
const updatedCookies = [...authCookies, serialize(TEMP_COOKIE, '', { maxAge: -1, path: '/' })]
|
||||
|
||||
res.setHeader("Set-Cookie", updatedCookies);
|
||||
res.setHeader('Set-Cookie', updatedCookies)
|
||||
}
|
||||
|
||||
const isDefined = <T>(value: T | undefined): value is T => !!value;
|
||||
const isDefined = <T>(value: T | undefined): value is T => !!value
|
||||
|
||||
const TEMP_TOKEN_SECRET = process.env.TEMP_TOKEN_SECRET!;
|
||||
const TEMP_COOKIE = "as-mfa-cookie";
|
||||
const ACCESS_TOKEN_COOKIE = "sb-access-token";
|
||||
const REFRESH_TOKEN_COOKIE = "sb-refresh-token";
|
||||
const TEMP_TOKEN_SECRET = process.env.TEMP_TOKEN_SECRET!
|
||||
const TEMP_COOKIE = 'as-mfa-cookie'
|
||||
const ACCESS_TOKEN_COOKIE = 'sb-access-token'
|
||||
const REFRESH_TOKEN_COOKIE = 'sb-refresh-token'
|
||||
```
|
||||
|
||||
## Step 7: Building the UI
|
||||
@@ -216,34 +205,34 @@ We will add some form components for signing in and signing up as well as a basi
|
||||
|
||||
Add the following code to `/pages/sign-up.tsx`:
|
||||
|
||||
```
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
```ts
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function SignUpPage() {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<main>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const target = e.target as typeof e.target & {
|
||||
email: { value: string };
|
||||
password: { value: string };
|
||||
};
|
||||
email: { value: string }
|
||||
password: { value: string }
|
||||
}
|
||||
|
||||
const email = target.email.value;
|
||||
const password = target.password.value;
|
||||
const email = target.email.value
|
||||
const password = target.password.value
|
||||
|
||||
await fetch("/api/sign-up", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
await fetch('/api/sign-up', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then((res) => res.json());
|
||||
}).then((res) => res.json())
|
||||
|
||||
router.push("/");
|
||||
router.push('/')
|
||||
}}
|
||||
>
|
||||
<label htmlFor="email">Email</label>
|
||||
@@ -253,49 +242,49 @@ export default function SignUpPage() {
|
||||
<button type="submit">Sign up</button>
|
||||
</form>
|
||||
<div>
|
||||
{"Already have an account? "}
|
||||
{'Already have an account? '}
|
||||
<Link href="sign-in">
|
||||
<a>Sign in</a>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Then add the following code to `/pages/sign-in.tsx`:
|
||||
|
||||
```
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
```ts
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function SignInPage() {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<main>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const target = e.target as typeof e.target & {
|
||||
email: { value: string };
|
||||
password: { value: string };
|
||||
};
|
||||
email: { value: string }
|
||||
password: { value: string }
|
||||
}
|
||||
|
||||
const email = target.email.value;
|
||||
const password = target.password.value;
|
||||
const email = target.email.value
|
||||
const password = target.password.value
|
||||
|
||||
const { state, mfaUrl } = await fetch("/api/sign-in", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const { state, mfaUrl } = await fetch('/api/sign-in', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then((res) => res.json());
|
||||
}).then((res) => res.json())
|
||||
|
||||
if (state === "CHALLENGE_REQUIRED") {
|
||||
window.location.href = mfaUrl;
|
||||
if (state === 'CHALLENGE_REQUIRED') {
|
||||
window.location.href = mfaUrl
|
||||
} else {
|
||||
router.push("/");
|
||||
router.push('/')
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -312,38 +301,38 @@ export default function SignInPage() {
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Now we will use Supabase's `withPageAuth` wrapper around `getServerSideProps` to make the home page require authentication via SSR. Replace the existing code in `/pages/index.tsx` with the following:
|
||||
|
||||
```
|
||||
import { getUser, User, withPageAuth } from "@supabase/auth-helpers-nextjs";
|
||||
import { GetServerSideProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { authsignal } from "../lib/authsignal";
|
||||
```ts
|
||||
import { getUser, User, withPageAuth } from '@supabase/auth-helpers-nextjs'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { authsignal } from '../lib/authsignal'
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
isEnrolled: boolean;
|
||||
user: User
|
||||
isEnrolled: boolean
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = withPageAuth({
|
||||
redirectTo: "/sign-in",
|
||||
redirectTo: '/sign-in',
|
||||
async getServerSideProps(ctx) {
|
||||
const { user } = await getUser(ctx);
|
||||
const { user } = await getUser(ctx)
|
||||
|
||||
const { isEnrolled } = await authsignal.getUser({userId: user.id});
|
||||
const { isEnrolled } = await authsignal.getUser({ userId: user.id })
|
||||
|
||||
return {
|
||||
props: { user, isEnrolled },
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export default function HomePage({ user, isEnrolled }: Props) {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<main>
|
||||
@@ -351,29 +340,29 @@ export default function HomePage({ user, isEnrolled }: Props) {
|
||||
<div> Signed in as: {user?.email}</div>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const { mfaUrl } = await fetch("/api/mfa", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const { mfaUrl } = await fetch('/api/mfa', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isEnrolled }),
|
||||
}).then((res) => res.json());
|
||||
}).then((res) => res.json())
|
||||
|
||||
window.location.href = mfaUrl;
|
||||
window.location.href = mfaUrl
|
||||
}}
|
||||
>
|
||||
{isEnrolled ? "Manage MFA settings" : "Set up MFA"}
|
||||
{isEnrolled ? 'Manage MFA settings' : 'Set up MFA'}
|
||||
</button>
|
||||
<button onClick={() => router.push("/api/sign-out")}>Sign out</button>
|
||||
<button onClick={() => router.push('/api/sign-out')}>Sign out</button>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Optional: To make things look a bit nicer, you can add the following to `/styles/globals.css`:
|
||||
|
||||
```
|
||||
```css
|
||||
main {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
@@ -435,113 +424,95 @@ Now we'll replace the existing api routes in `/pages/api/` with 5 new routes:
|
||||
|
||||
Add the following code to `/pages/api/sign-in.ts`:
|
||||
|
||||
```
|
||||
import { supabaseClient } from "@supabase/auth-helpers-nextjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authsignal } from "../../lib/authsignal";
|
||||
import { setAuthCookie, setTempCookie } from "../../lib/cookies";
|
||||
```ts
|
||||
import { supabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authsignal } from '../../lib/authsignal'
|
||||
import { setAuthCookie, setTempCookie } from '../../lib/cookies'
|
||||
|
||||
export default async function signIn(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { email, password } = req.body;
|
||||
export default async function signIn(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { email, password } = req.body
|
||||
|
||||
const { data, error } = await supabaseClient.auth.api.signInWithEmail(
|
||||
email,
|
||||
password
|
||||
);
|
||||
const { data, error } = await supabaseClient.auth.api.signInWithEmail(email, password)
|
||||
|
||||
if (error || !data?.user) {
|
||||
return res.send({ error });
|
||||
return res.send({ error })
|
||||
}
|
||||
|
||||
const { state, url: mfaUrl } = await authsignal.track({
|
||||
action: "signIn",
|
||||
action: 'signIn',
|
||||
userId: data.user.id,
|
||||
});
|
||||
})
|
||||
|
||||
if (state === "CHALLENGE_REQUIRED") {
|
||||
await setTempCookie(data, res);
|
||||
if (state === 'CHALLENGE_REQUIRED') {
|
||||
await setTempCookie(data, res)
|
||||
} else {
|
||||
setAuthCookie(data, res);
|
||||
setAuthCookie(data, res)
|
||||
}
|
||||
|
||||
res.send({ state, mfaUrl });
|
||||
res.send({ state, mfaUrl })
|
||||
}
|
||||
```
|
||||
|
||||
Then to handle new sign-ups add the following to `/pages/api/sign-up.ts`:
|
||||
|
||||
```
|
||||
import { supabaseClient } from "@supabase/auth-helpers-nextjs";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { setAuthCookie } from "../../lib/cookies";
|
||||
```ts
|
||||
import { supabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { setAuthCookie } from '../../lib/cookies'
|
||||
|
||||
export default async function signUp(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { email, password } = req.body;
|
||||
export default async function signUp(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { email, password } = req.body
|
||||
|
||||
const { data, error } = await supabaseClient.auth.api.signUpWithEmail(
|
||||
email,
|
||||
password
|
||||
);
|
||||
const { data, error } = await supabaseClient.auth.api.signUpWithEmail(email, password)
|
||||
|
||||
if (error || !isSession(data)) {
|
||||
res.send({ error });
|
||||
res.send({ error })
|
||||
} else {
|
||||
setAuthCookie(data, res);
|
||||
res.send({ data });
|
||||
setAuthCookie(data, res)
|
||||
res.send({ data })
|
||||
}
|
||||
}
|
||||
|
||||
const isSession = (data: any): data is Session => !!data?.access_token;
|
||||
const isSession = (data: any): data is Session => !!data?.access_token
|
||||
```
|
||||
|
||||
To clear the auth cookies on sign-out add the following to `/pages/api/sign-out.ts`:
|
||||
|
||||
```
|
||||
import { supabaseClient } from "@supabase/auth-helpers-nextjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
```ts
|
||||
import { supabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default async function signOut(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
supabaseClient.auth.api.deleteAuthCookie(req, res, { redirectTo: "/sign-in" });
|
||||
export default async function signOut(req: NextApiRequest, res: NextApiResponse) {
|
||||
supabaseClient.auth.api.deleteAuthCookie(req, res, { redirectTo: '/sign-in' })
|
||||
}
|
||||
```
|
||||
|
||||
To handle the user's actions to set up MFA or manage their existing MFA settings, add the following to `/pages/api/mfa.ts`:
|
||||
|
||||
```
|
||||
import { getUser, withApiAuth } from "@supabase/auth-helpers-nextjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authsignal } from "../../lib/authsignal";
|
||||
```ts
|
||||
import { getUser, withApiAuth } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authsignal } from '../../lib/authsignal'
|
||||
|
||||
export default withApiAuth(async function mfa(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).send({ message: "Only POST requests allowed" });
|
||||
export default withApiAuth(async function mfa(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).send({ message: 'Only POST requests allowed' })
|
||||
}
|
||||
|
||||
const { user } = await getUser({ req, res });
|
||||
const { user } = await getUser({ req, res })
|
||||
|
||||
const { isEnrolled } = req.body;
|
||||
const { isEnrolled } = req.body
|
||||
|
||||
const { url: mfaUrl } = await authsignal.track({
|
||||
action: isEnrolled ? "manageSettings" : "enroll",
|
||||
action: isEnrolled ? 'manageSettings' : 'enroll',
|
||||
userId: user.id,
|
||||
redirectToSettings: isEnrolled,
|
||||
});
|
||||
})
|
||||
|
||||
res.send({ mfaUrl });
|
||||
});
|
||||
res.send({ mfaUrl })
|
||||
})
|
||||
```
|
||||
|
||||
Because the user should be authenticated with Supabase to set up or manage MFA, we can use Supabase's `withApiAuth` wrapper to protect this route.
|
||||
@@ -550,28 +521,25 @@ The `redirectToSettings` param specifies whether the user should be redirected t
|
||||
|
||||
Finally we need a route to handle the redirect back from Authsignal after an MFA challenge. Add the following to `/pages/api/callback.ts`:
|
||||
|
||||
```
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authsignal } from "../../lib/authsignal";
|
||||
import { getSessionFromTempCookie, setAuthCookie } from "../../lib/cookies";
|
||||
```ts
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authsignal } from '../../lib/authsignal'
|
||||
import { getSessionFromTempCookie, setAuthCookie } from '../../lib/cookies'
|
||||
|
||||
export default async function callback(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const token = req.query.token as string;
|
||||
export default async function callback(req: NextApiRequest, res: NextApiResponse) {
|
||||
const token = req.query.token as string
|
||||
|
||||
const { success } = await authsignal.validateChallenge({ token });
|
||||
const { success } = await authsignal.validateChallenge({ token })
|
||||
|
||||
if (success) {
|
||||
const session = await getSessionFromTempCookie(req);
|
||||
const session = await getSessionFromTempCookie(req)
|
||||
|
||||
if (session) {
|
||||
setAuthCookie(session, res);
|
||||
setAuthCookie(session, res)
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect("/");
|
||||
res.redirect('/')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -39,12 +39,7 @@ Once you have added both to WeWeb, you will have the option to enable realtime t
|
||||
|
||||
**🚨 Warning 🚨**
|
||||
|
||||
> Realtime is disabled by default in Supabase for better database performance and security. Learn more about [realtime functionalities](https://supabase.com/docs/guides/database#realtime) in our documentation.
|
||||
> Member
|
||||
> @awalias awalias 16 days ago
|
||||
> might just wanna change these ones to be relative but otherwise looks great 👍
|
||||
|
||||
@EWMAs Reply...
|
||||
> Realtime is disabled by default in Supabase for better database performance and security. Learn more about [realtime functionalities](/docs/guides/realtime).
|
||||
|
||||
## Step 2: GET Data from Supabase
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ We realize that database migrations are difficult, and this is one of the proble
|
||||
|
||||
While Supabase officially supports Docker, we have several other deployment strategies managed by the community:
|
||||
|
||||
- [supabase-docker](../../guides/hosting/docker) (Official)
|
||||
- [supabase-docker](/docs/guides/self-hosting/docker) (Official)
|
||||
- [supabase-kubernetes](https://github.com/supabase-community/supabase-kubernetes) (Unofficial)
|
||||
- [supabase-terraform](https://github.com/supabase-community/supabase-terraform) (Unofficial)
|
||||
- [supabase-traefik](https://github.com/supabase-community/supabase-traefik) (Unofficial)
|
||||
|
||||
@@ -11,7 +11,7 @@ Docker is the easiest way to get started with self-hosted Supabase.
|
||||
|
||||
You need the following installed in your system:
|
||||
|
||||
- [Docker](https://docs.docker.com/engine/install/) and [docker-compose](https://docs.docker.com/compose/install/)
|
||||
- [Docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/install/)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
## Quick Start
|
||||
@@ -31,7 +31,7 @@ cd supabase/docker
|
||||
cp .env.example .env
|
||||
|
||||
# Start
|
||||
docker-compose up
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Now visit [http://localhost:3000](http://localhost:3000) to start using Supabase Studio.
|
||||
@@ -97,7 +97,7 @@ Once you have done this, you can safely comment out the `db` section of the `doc
|
||||
|
||||
### Setting database's `log_min_messages`
|
||||
|
||||
By default, `docker-compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime.
|
||||
By default, `docker compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime.
|
||||
However, you might miss important log messages such as database errors. Configure `log_min_messages` based on your needs.
|
||||
|
||||
### File storage backend on macOS
|
||||
|
||||
@@ -34,6 +34,7 @@ Kevin Grüneberg
|
||||
Lakshmipathi G
|
||||
Laura C
|
||||
Leo T
|
||||
Long Hoang
|
||||
Margarita Sandomirskaia
|
||||
Marijana Šimag
|
||||
Mark Burggraf
|
||||
@@ -60,7 +61,7 @@ ____________
|
||||
|
||||
JOBS
|
||||
|
||||
If you are interested in working with us check out the open roles here: https://about.supabase.com/careers
|
||||
If you are interested in working with us check out the open roles here: https://supabase.com/careers
|
||||
|
||||
___________
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@ GitHub stars rocketed for our two main repos, [supabase](https://github.com/supa
|
||||
|
||||
Here are the things that survived well.
|
||||
|
||||
### Middleware: docker-compose up
|
||||
### Middleware: docker compose up
|
||||
|
||||
This was the most surprising survivor. Our middleware was served from a single Ubuntu server with 4 CPUs and 8GB of RAM. This server was running our middleware using `docker-compose up`:
|
||||
This was the most surprising survivor. Our middleware was served from a single Ubuntu server with 4 CPUs and 8GB of RAM. This server was running our middleware using `docker compose up`:
|
||||
|
||||

|
||||

|
||||
|
||||
In case you're wondering why any sane company would use that in production, it's because we weren't planning to launch - the HackerNews post was created by an early GitHub follower, while we were alpha testing, and it was too scary to migrate the middleware while it was servicing the thundering herd. All Supabase projects use the same middleware stack (sans docker-compose), so I guess this counts as as a successful load test.
|
||||
In case you're wondering why any sane company would use that in production, it's because we weren't planning to launch - the HackerNews post was created by an early GitHub follower, while we were alpha testing, and it was too scary to migrate the middleware while it was servicing the thundering herd. All Supabase projects use the same middleware stack (without docker compose), so I guess this counts as a successful load test.
|
||||
|
||||
We have since migrated our middleware to multiple ECS clusters, globally load-balanced using AWS's [Global Accelerator](https://aws.amazon.com/global-accelerator/).
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ Also check `kong.yml` inside the `kong` directory where you'll see how all the s
|
||||
Once you're all set, you can start the stack by running:
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Head over to the [Self Hosting Docs](/docs/guides/self-hosting) for a more complete walk through, it also includes several [one-click deploys](/docs/guides/self-hosting#one-click-deploys), so you can easily deploy into your own cloud hosting provider.
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
[
|
||||
{
|
||||
"text": "Documentation",
|
||||
"description": "Everything you need that makes it simple to get started.",
|
||||
"description": "Everything you need to start building with Supabase.",
|
||||
"url": "/docs",
|
||||
"icon": "M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
||||
},
|
||||
{
|
||||
"text": "API reference",
|
||||
"description": "Examples and references for using API libraries.",
|
||||
"url": "/docs/client/supabase-client",
|
||||
"icon": "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
},
|
||||
{
|
||||
"text": "Guides",
|
||||
"description": "Examples and references for using Supabase.",
|
||||
"url": "/docs/",
|
||||
"text": "Changelog",
|
||||
"description": "See the latest updates and product improvements.",
|
||||
"url": "/changelog",
|
||||
"icon": "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"
|
||||
},
|
||||
{
|
||||
"text": "Careers",
|
||||
"description": "Join the Supabase team and get involved.",
|
||||
"url": "https://supabase.com/careers",
|
||||
"url": "/careers",
|
||||
"icon": "M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
}
|
||||
]
|
||||
@@ -1787,16 +1787,6 @@ module.exports = [
|
||||
source: '/docs/guides/with-ionic-angular',
|
||||
destination: '/docs/guides/getting-started/tutorials/with-ionic-angular',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/examples',
|
||||
destination: '/docs/guides/resources/examples',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/examples',
|
||||
destination: '/docs/guides/resources/examples',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/hosting/overview',
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function launchweek() {
|
||||
const { isDarkMode } = useTheme()
|
||||
const title = 'Launch Week 6'
|
||||
const description = 'Supabase Launch Week 6 | 12-18 Dec 2022'
|
||||
const liveDay = 'Friday'
|
||||
const liveDay = null
|
||||
|
||||
const [supabase] = useState(() =>
|
||||
createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
|
||||
@@ -219,11 +219,6 @@ export default function launchweek() {
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<a className="flex items-center justify-center mb-20" href="#currentDay">
|
||||
<div className="border border-slate-600 text-black dark:text-white text-sm rounded-full bg-slate-400 px-2 py-1 hover:bg-slate-600">
|
||||
Jump to current day
|
||||
</div>
|
||||
</a>
|
||||
{process.env.NEXT_PUBLIC_LW_STARTED && (
|
||||
<>
|
||||
<SectionContainer className="!py-0 ">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Usage
|
||||
# Start: docker-compose up
|
||||
# With helpers: docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up
|
||||
# Stop: docker-compose down
|
||||
# Destroy: docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
|
||||
# Start: docker compose up
|
||||
# With helpers: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up
|
||||
# Stop: docker compose down
|
||||
# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
|
||||
|
||||
version: "3.8"
|
||||
|
||||
|
||||
Generated
+61
@@ -37607,6 +37607,7 @@
|
||||
"uniforms-bootstrap4": "^3.6.2",
|
||||
"uniforms-bridge-json-schema": "^3.6.2",
|
||||
"uuid": "^8.3.2",
|
||||
"valtio": "^1.7.6",
|
||||
"yup": "^0.32.11",
|
||||
"yup-password": "^0.2.2",
|
||||
"zxcvbn": "^4.4.2"
|
||||
@@ -37829,6 +37830,11 @@
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz",
|
||||
"integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw=="
|
||||
},
|
||||
"studio/node_modules/proxy-compare": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.3.0.tgz",
|
||||
"integrity": "sha512-c3L2CcAi7f7pvlD0D7xsF+2CQIW8C3HaYx2Pfgq8eA4HAl3GAH6/dVYsyBbYF/0XJs2ziGLrzmz5fmzPm6A0pQ=="
|
||||
},
|
||||
"studio/node_modules/react-resize-detector": {
|
||||
"version": "6.7.8",
|
||||
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz",
|
||||
@@ -37889,6 +37895,46 @@
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/valtio": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.7.6.tgz",
|
||||
"integrity": "sha512-zsGrCCYOIpy8egQxftduFyJusF/BMu3CganhHKUOE/I6t6V6yA1MDfZZkrYoWYCGkC3rSBYcIHEEsoYQM9lV2w==",
|
||||
"dependencies": {
|
||||
"proxy-compare": "2.3.0",
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/helper-module-imports": ">=7.12",
|
||||
"@babel/types": ">=7.13",
|
||||
"aslemammad-vite-plugin-macro": ">=1.0.0-alpha.1",
|
||||
"babel-plugin-macros": ">=3.0",
|
||||
"react": ">=16.8",
|
||||
"vite": ">=2.8.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/helper-module-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"optional": true
|
||||
},
|
||||
"aslemammad-vite-plugin-macro": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"studio/node_modules/webpack-bundle-analyzer": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz",
|
||||
@@ -63371,6 +63417,7 @@
|
||||
"uniforms-bootstrap4": "^3.6.2",
|
||||
"uniforms-bridge-json-schema": "^3.6.2",
|
||||
"uuid": "^8.3.2",
|
||||
"valtio": "^1.7.6",
|
||||
"yup": "^0.32.11",
|
||||
"yup-password": "^0.2.2",
|
||||
"zxcvbn": "^4.4.2"
|
||||
@@ -63521,6 +63568,11 @@
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz",
|
||||
"integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw=="
|
||||
},
|
||||
"proxy-compare": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.3.0.tgz",
|
||||
"integrity": "sha512-c3L2CcAi7f7pvlD0D7xsF+2CQIW8C3HaYx2Pfgq8eA4HAl3GAH6/dVYsyBbYF/0XJs2ziGLrzmz5fmzPm6A0pQ=="
|
||||
},
|
||||
"react-resize-detector": {
|
||||
"version": "6.7.8",
|
||||
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz",
|
||||
@@ -63564,6 +63616,15 @@
|
||||
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"valtio": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.7.6.tgz",
|
||||
"integrity": "sha512-zsGrCCYOIpy8egQxftduFyJusF/BMu3CganhHKUOE/I6t6V6yA1MDfZZkrYoWYCGkC3rSBYcIHEEsoYQM9lV2w==",
|
||||
"requires": {
|
||||
"proxy-compare": "2.3.0",
|
||||
"use-sync-external-store": "1.2.0"
|
||||
}
|
||||
},
|
||||
"webpack-bundle-analyzer": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz",
|
||||
|
||||
+4
-4
@@ -22,10 +22,10 @@
|
||||
"dev:www": "turbo run dev --filter=www --parallel",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"apps/**/*.{js,jsx,ts,tsx,css,md,json}\"",
|
||||
"docker:dev": "cd docker && docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up --renew-anon-volumes",
|
||||
"docker:up": "cd docker && docker-compose up",
|
||||
"docker:down": "cd docker && docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down --remove-orphans",
|
||||
"docker:remove": "cd docker && docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml rm -vfs",
|
||||
"docker:dev": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up --renew-anon-volumes",
|
||||
"docker:up": "cd docker && docker compose up",
|
||||
"docker:down": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down --remove-orphans",
|
||||
"docker:remove": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml rm -vfs",
|
||||
"test": "jest",
|
||||
"test:studio": "turbo run test --filter=studio",
|
||||
"perf:kong": "ab -t 5 -c 20 -T application/json http://localhost:8000/",
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ As such, the features exposed on Studio for existing deployments are limited to
|
||||
|
||||
## Managing Project Settings
|
||||
|
||||
Project settings are managed outside of the Dashboard. If you use docker-compose, you should manage the settings in your docker-compose file. If you're deploying Supabase to your own cloud, you should store your secrets and env vars in a vault or secrets manager.
|
||||
Project settings are managed outside of the Dashboard. If you use docker compose, you should manage the settings in your docker-compose file. If you're deploying Supabase to your own cloud, you should store your secrets and env vars in a vault or secrets manager.
|
||||
|
||||
## How to contribute?
|
||||
|
||||
@@ -51,7 +51,7 @@ Follow the [self-hosting guide](https://supabase.com/docs/guides/hosting/docker)
|
||||
```
|
||||
cd ..
|
||||
cd docker
|
||||
docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up
|
||||
docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up
|
||||
```
|
||||
|
||||
Once you've got that set up, update `.env` in the studio folder with the corresponding values.
|
||||
|
||||
+80
-29
@@ -1,9 +1,11 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { IconLoader, Modal } from 'ui'
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { loadStripe } from '@stripe/stripe-js'
|
||||
import { Elements } from '@stripe/react-stripe-js'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { Modal } from 'ui'
|
||||
import { useStore } from 'hooks'
|
||||
import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, STRIPE_PUBLIC_KEY } from 'lib/constants'
|
||||
import AddPaymentMethodForm from './AddPaymentMethodForm'
|
||||
@@ -20,16 +22,49 @@ const AddNewPaymentMethodModal: FC<Props> = ({ visible, returnUrl, onCancel }) =
|
||||
const { ui } = useStore()
|
||||
const [intent, setIntent] = useState<any>()
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setupIntent()
|
||||
}
|
||||
}, [visible])
|
||||
const captchaLoaded = useIsHCaptchaLoaded()
|
||||
|
||||
const setupIntent = async () => {
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const [captchaRef, setCaptchaRef] = useState<HCaptcha | null>(null)
|
||||
|
||||
const captchaRefCallback = useCallback((node) => {
|
||||
setCaptchaRef(node)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const loadPaymentForm = async () => {
|
||||
if (visible && captchaRef && captchaLoaded) {
|
||||
let token = captchaToken
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
const captchaResponse = await captchaRef.execute({ async: true })
|
||||
token = captchaResponse?.response ?? null
|
||||
}
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
await setupIntent(token ?? undefined)
|
||||
resetCaptcha()
|
||||
}
|
||||
}
|
||||
|
||||
loadPaymentForm()
|
||||
}, [visible, captchaRef, captchaLoaded])
|
||||
|
||||
const resetCaptcha = () => {
|
||||
setCaptchaToken(null)
|
||||
captchaRef?.resetCaptcha()
|
||||
}
|
||||
|
||||
const setupIntent = async (hcaptchaToken: string | undefined) => {
|
||||
setIntent(undefined)
|
||||
|
||||
const orgSlug = ui.selectedOrganization?.slug ?? ''
|
||||
const intent = await post(`${API_URL}/organizations/${orgSlug}/payments/setup-intent`, {})
|
||||
const intent = await post(`${API_URL}/organizations/${orgSlug}/payments/setup-intent`, {
|
||||
hcaptchaToken,
|
||||
})
|
||||
|
||||
if (intent.error) {
|
||||
return ui.setNotification({
|
||||
@@ -37,9 +72,9 @@ const AddNewPaymentMethodModal: FC<Props> = ({ visible, returnUrl, onCancel }) =
|
||||
message: intent.error.message,
|
||||
error: intent.error,
|
||||
})
|
||||
} else {
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
const options = {
|
||||
@@ -47,27 +82,43 @@ const AddNewPaymentMethodModal: FC<Props> = ({ visible, returnUrl, onCancel }) =
|
||||
appearance: { theme: 'night', labels: 'floating' },
|
||||
} as any
|
||||
|
||||
const onLocalCancel = () => {
|
||||
setIntent(undefined)
|
||||
return onCancel()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hideFooter
|
||||
size="medium"
|
||||
visible={visible}
|
||||
header="Add new payment method"
|
||||
onCancel={onCancel}
|
||||
className="PAYMENT"
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
{intent !== undefined ? (
|
||||
// We cant display the hCaptcha in the modal, as the modal auto-closes when clicking the captcha
|
||||
// So we only show the modal if the captcha has been executed successfully (intent loaded)
|
||||
<>
|
||||
<HCaptcha
|
||||
ref={captchaRefCallback}
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
size="invisible"
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token)
|
||||
}}
|
||||
onClose={onLocalCancel}
|
||||
onExpire={() => {
|
||||
setCaptchaToken(null)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
hideFooter
|
||||
size="medium"
|
||||
visible={visible && intent !== undefined}
|
||||
header="Add new payment method"
|
||||
onCancel={onLocalCancel}
|
||||
className="PAYMENT"
|
||||
>
|
||||
<div className="py-4 space-y-4">
|
||||
<Elements stripe={stripePromise} options={options}>
|
||||
<AddPaymentMethodForm returnUrl={returnUrl} onCancel={onCancel} />
|
||||
<AddPaymentMethodForm returnUrl={returnUrl} onCancel={onLocalCancel} />
|
||||
</Elements>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-center py-20">
|
||||
<IconLoader size={16} className="animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ export const formSubscriptionUpdatePayload = (
|
||||
},
|
||||
nonChangeableAddons: SubscriptionAddon[],
|
||||
selectedPaymentMethod: string,
|
||||
region: string
|
||||
region: string,
|
||||
hcaptchaToken: string | undefined
|
||||
) => {
|
||||
const { computeSize, pitrDuration, customDomains } = selectedAddons
|
||||
|
||||
@@ -78,6 +79,7 @@ export const formSubscriptionUpdatePayload = (
|
||||
addons,
|
||||
proration_date,
|
||||
payment_method: selectedPaymentMethod,
|
||||
hcaptchaToken
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { FC, useState, useEffect, useRef } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Transition } from '@headlessui/react'
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useStore, useFlag } from 'hooks'
|
||||
import { getURL } from 'lib/helpers'
|
||||
import { post, patch } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
|
||||
import Divider from 'components/ui/Divider'
|
||||
import { SubscriptionAddon } from './AddOns/AddOns.types'
|
||||
@@ -45,6 +46,9 @@ const EnterpriseUpdate: FC<Props> = ({
|
||||
const isCustomDomainsEnabled = useFlag('customDomains')
|
||||
const isPITRSelfServeEnabled = useFlag('pitrSelfServe')
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
|
||||
const projectId = ui.selectedProject?.id ?? -1
|
||||
const projectRef = ui.selectedProject?.ref ?? 'default'
|
||||
const projectRegion = ui.selectedProject?.region ?? ''
|
||||
@@ -110,7 +114,8 @@ const EnterpriseUpdate: FC<Props> = ({
|
||||
selectedAddons,
|
||||
nonChangeableAddons,
|
||||
selectedPaymentMethodId,
|
||||
projectRegion
|
||||
projectRegion,
|
||||
undefined
|
||||
),
|
||||
tier: currentSubscription.tier.price_id,
|
||||
}
|
||||
@@ -127,8 +132,45 @@ const EnterpriseUpdate: FC<Props> = ({
|
||||
setIsRefreshingPreview(false)
|
||||
}
|
||||
|
||||
const resetCaptcha = () => {
|
||||
setCaptchaToken(null)
|
||||
captchaRef.current?.resetCaptcha()
|
||||
}
|
||||
|
||||
const beforeConfirmPayment = async (): Promise<boolean> => {
|
||||
setIsSubmitting(true)
|
||||
let token = captchaToken
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
const captchaResponse = await captchaRef.current?.execute({ async: true })
|
||||
token = captchaResponse?.response ?? null
|
||||
setCaptchaToken(token)
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSubmitting(false)
|
||||
return false
|
||||
}
|
||||
|
||||
setIsSubmitting(false)
|
||||
return true
|
||||
}
|
||||
|
||||
// Last todo to support enterprise billing on dashboard + E2E test
|
||||
const onConfirmPayment = async () => {
|
||||
setIsSubmitting(true)
|
||||
let token = captchaToken
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
const captchaResponse = await captchaRef.current?.execute({ async: true })
|
||||
token = captchaResponse?.response ?? null
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...formSubscriptionUpdatePayload(
|
||||
currentSubscription,
|
||||
@@ -136,13 +178,14 @@ const EnterpriseUpdate: FC<Props> = ({
|
||||
selectedAddons,
|
||||
nonChangeableAddons,
|
||||
selectedPaymentMethodId,
|
||||
projectRegion
|
||||
projectRegion,
|
||||
token ?? undefined
|
||||
),
|
||||
tier: currentSubscription.tier.price_id,
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
const res = await patch(`${API_URL}/projects/${projectRef}/subscription`, payload)
|
||||
resetCaptcha()
|
||||
|
||||
if (res?.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
@@ -272,7 +315,21 @@ const EnterpriseUpdate: FC<Props> = ({
|
||||
onSelectAddNewPaymentMethod={() => {
|
||||
setShowAddPaymentMethodModal(true)
|
||||
}}
|
||||
beforeConfirmPayment={beforeConfirmPayment}
|
||||
onConfirmPayment={onConfirmPayment}
|
||||
captcha={
|
||||
<HCaptcha
|
||||
ref={captchaRef}
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
size="invisible"
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token)
|
||||
}}
|
||||
onExpire={() => {
|
||||
setCaptchaToken(null)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CANCELLATION_REASONS } from '../Billing.constants'
|
||||
import { UpdateSuccess } from '../'
|
||||
import { SubscriptionPreview } from '../Billing.types'
|
||||
import { StripeSubscription } from 'components/interfaces/Billing'
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
|
||||
interface Props {
|
||||
freeTier: any
|
||||
@@ -24,6 +25,9 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
const projectId = ui.selectedProject?.id ?? -1
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
|
||||
const formRef = useRef<any>()
|
||||
|
||||
const initialValues = { message: '' }
|
||||
@@ -38,7 +42,7 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
const [subscriptionPreview, setSubscriptionPreview] = useState<SubscriptionPreview>()
|
||||
|
||||
// Anything above a micro instance size will involve a change in compute size
|
||||
const willChangeComputeSize = (subscription?.addons ?? []).length > 0
|
||||
// as downgrading back to free will bring the project back to micro
|
||||
const currentComputeSize = subscription?.addons.find((option) =>
|
||||
option.supabase_prod_id.includes('addon_instance')
|
||||
)
|
||||
@@ -59,7 +63,7 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getDowngradeSuccessMessage = () => {
|
||||
const getDowngradeRefundMessage = () => {
|
||||
return `A total of $${returnedAmount} will be refunded as credits on ${billingDate.toLocaleDateString(
|
||||
'en-US',
|
||||
{
|
||||
@@ -88,15 +92,33 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (willChangeComputeSize) {
|
||||
setIsSubmitting(true)
|
||||
let token = captchaToken
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
const captchaResponse = await captchaRef.current?.execute({ async: true })
|
||||
token = captchaResponse?.response ?? null
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (currentComputeSize !== undefined) {
|
||||
setMessage(values.message)
|
||||
return setShowConfirmModal(true)
|
||||
} else {
|
||||
downgradeProject(values)
|
||||
downgradeProject(values, token as string)
|
||||
}
|
||||
}
|
||||
|
||||
const downgradeProject = async (values?: any) => {
|
||||
const resetCaptcha = () => {
|
||||
setCaptchaToken(null)
|
||||
captchaRef.current?.resetCaptcha()
|
||||
}
|
||||
|
||||
const downgradeProject = async (values?: any, hcaptchaToken?: string) => {
|
||||
const downgradeMessage = values?.message ?? message
|
||||
|
||||
try {
|
||||
@@ -110,8 +132,11 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
tier,
|
||||
addons,
|
||||
proration_date,
|
||||
hcaptchaToken: captchaToken ?? hcaptchaToken,
|
||||
})
|
||||
|
||||
resetCaptcha()
|
||||
|
||||
if (res?.error) {
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
@@ -119,11 +144,11 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
error: res.error,
|
||||
})
|
||||
} else {
|
||||
if (willChangeComputeSize) {
|
||||
if (currentComputeSize !== undefined) {
|
||||
app.onProjectStatusUpdated(projectId, PROJECT_STATUS.RESTORING)
|
||||
ui.setNotification({
|
||||
category: 'info',
|
||||
message: getDowngradeSuccessMessage(),
|
||||
message: getDowngradeRefundMessage(),
|
||||
duration: 8000,
|
||||
})
|
||||
ui.setNotification({
|
||||
@@ -139,9 +164,9 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
}
|
||||
const feedbackRes = await post(`${API_URL}/feedback/downgrade`, {
|
||||
projectRef,
|
||||
reasons: selectedReasons.reduce((a, b) => `${a}- ${b}\n`, ''),
|
||||
additionalFeedback: downgradeMessage,
|
||||
exitAction: 'downgrade',
|
||||
reasons: selectedReasons.reduce((a, b) => `${a}- ${b}\n`, ''),
|
||||
additionalFeedback: downgradeMessage,
|
||||
exitAction: 'downgrade',
|
||||
})
|
||||
if (feedbackRes.error) throw feedbackRes.error
|
||||
} catch (error: any) {
|
||||
@@ -159,7 +184,7 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
<UpdateSuccess
|
||||
projectRef={projectRef || ''}
|
||||
title="Your project has been updated"
|
||||
message={getDowngradeSuccessMessage()}
|
||||
message={getDowngradeRefundMessage()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -173,7 +198,7 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
enterFrom="transform opacity-0 translate-x-10"
|
||||
enterTo="transform opacity-100 translate-x-0"
|
||||
>
|
||||
<div className="w-4/5 space-y-8">
|
||||
<div className="w-full xl:w-4/5 space-y-8">
|
||||
<div ref={formRef} className="relative">
|
||||
<div className="absolute top-[2px] -left-24">
|
||||
<Button type="text" icon={<IconArrowLeft />} onClick={onSelectBack}>
|
||||
@@ -245,6 +270,19 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
credits
|
||||
</p>
|
||||
</div>
|
||||
<div className="self-center">
|
||||
<HCaptcha
|
||||
ref={captchaRef}
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
size="invisible"
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token)
|
||||
}}
|
||||
onExpire={() => {
|
||||
setCaptchaToken(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { FC, useRef, useState } from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Listbox, IconLoader, Button, IconPlus, IconAlertCircle, IconCreditCard } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
@@ -42,8 +42,11 @@ interface Props {
|
||||
isLoadingPaymentMethods: boolean
|
||||
onSelectPaymentMethod: (method: any) => void
|
||||
onSelectAddNewPaymentMethod: () => void
|
||||
beforeConfirmPayment: () => Promise<boolean>
|
||||
onConfirmPayment: () => void
|
||||
isSubmitting: boolean
|
||||
|
||||
captcha: React.ReactNode
|
||||
}
|
||||
|
||||
// Use case of this panel is actually only for upgrading from Free to Pro
|
||||
@@ -70,8 +73,10 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
isLoadingPaymentMethods,
|
||||
onSelectPaymentMethod,
|
||||
onSelectAddNewPaymentMethod,
|
||||
beforeConfirmPayment,
|
||||
onConfirmPayment,
|
||||
isSubmitting,
|
||||
captcha,
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
const projectRegion = ui.selectedProject?.region
|
||||
@@ -120,7 +125,7 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
const validateOrder = () => {
|
||||
const validateOrder = async () => {
|
||||
const error = validateSubscriptionUpdatePayload(selectedAddons)
|
||||
if (error) {
|
||||
return ui.setNotification({
|
||||
@@ -129,16 +134,23 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
message: error,
|
||||
})
|
||||
} else {
|
||||
isChangingComputeSize || (isChangingPITRDuration && selectedPITRDays === 0)
|
||||
? setShowConfirmModal(true)
|
||||
: onConfirmPayment()
|
||||
// [Joshen] We're validating captcha before subsequent actions as there's an issue
|
||||
// with the hcaptcha overlay and the modal component, such that clicking on the hcaptcha closes
|
||||
// both the hcaptcha overlay and the modal. Need to figure that out first, then we can validate
|
||||
// the hcaptcha within onConfirmPayment()
|
||||
const hasValidCaptcha = await beforeConfirmPayment()
|
||||
if (hasValidCaptcha) {
|
||||
isChangingComputeSize || (isChangingPITRDuration && selectedPITRDays === 0)
|
||||
? setShowConfirmModal(true)
|
||||
: onConfirmPayment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="w-full space-y-8 border-l bg-panel-body-light px-6 py-10 dark:bg-panel-body-dark lg:px-12 overflow-y-auto"
|
||||
className="w-full px-6 py-10 space-y-8 overflow-y-auto border-l bg-panel-body-light dark:bg-panel-body-dark lg:px-12"
|
||||
style={{ height: 'calc(100vh - 57px)' }}
|
||||
>
|
||||
<p>Payment Summary</p>
|
||||
@@ -293,7 +305,7 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-px w-full bg-scale-600" />
|
||||
<div className="w-full h-px bg-scale-600" />
|
||||
|
||||
<PaymentTotal
|
||||
subscriptionPreview={subscriptionPreview}
|
||||
@@ -305,12 +317,12 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">Select payment method</p>
|
||||
{isLoadingPaymentMethods ? (
|
||||
<div className="flex items-center space-x-4 rounded-md border border-scale-700 bg-scale-400 px-4 py-2">
|
||||
<div className="flex items-center px-4 py-2 space-x-4 border rounded-md border-scale-700 bg-scale-400">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<p className="text-sm text-scale-1100">Retrieving payment methods</p>
|
||||
</div>
|
||||
) : paymentMethods.length === 0 ? (
|
||||
<div className="flex items-center justify-between rounded-md border border-dashed bg-scale-100 px-4 py-2">
|
||||
<div className="flex items-center justify-between px-4 py-2 border border-dashed rounded-md bg-scale-100">
|
||||
<div className="flex items-center space-x-4 text-scale-1100">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm">No saved payment methods</p>
|
||||
@@ -370,11 +382,11 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
)
|
||||
})}
|
||||
<div
|
||||
className="group flex cursor-pointer items-center space-x-2 py-2 px-3 transition hover:bg-scale-500"
|
||||
className="flex items-center px-3 py-2 space-x-2 transition cursor-pointer group hover:bg-scale-500"
|
||||
onClick={onSelectAddNewPaymentMethod}
|
||||
>
|
||||
<IconPlus size={16} />
|
||||
<p className="text-scale-1000 transition group-hover:text-scale-1200">
|
||||
<p className="transition text-scale-1000 group-hover:text-scale-1200">
|
||||
Add new payment method
|
||||
</p>
|
||||
</div>
|
||||
@@ -382,6 +394,8 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="self-center">{captcha}</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger className="w-full">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from './AddOns/AddOns.utils'
|
||||
import BackButton from 'components/ui/BackButton'
|
||||
import SupportPlan from './AddOns/SupportPlan'
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
|
||||
// Do not allow compute size changes for af-south-1
|
||||
|
||||
@@ -52,6 +53,9 @@ const ProUpgrade: FC<Props> = ({
|
||||
const isCustomDomainsEnabled = useFlag('customDomains')
|
||||
const isPITRSelfServeEnabled = useFlag('pitrSelfServe')
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
|
||||
const { addons } = products
|
||||
const computeSizes = formatComputeSizes(addons)
|
||||
const pitrDurationOptions = formatPITROptions(addons)
|
||||
@@ -133,7 +137,8 @@ const ProUpgrade: FC<Props> = ({
|
||||
selectedAddons,
|
||||
nonChangeableAddons,
|
||||
selectedPaymentMethodId,
|
||||
projectRegion
|
||||
projectRegion,
|
||||
undefined
|
||||
)
|
||||
|
||||
setIsRefreshingPreview(true)
|
||||
@@ -148,6 +153,30 @@ const ProUpgrade: FC<Props> = ({
|
||||
setIsRefreshingPreview(false)
|
||||
}
|
||||
|
||||
const resetCaptcha = () => {
|
||||
setCaptchaToken(null)
|
||||
captchaRef.current?.resetCaptcha()
|
||||
}
|
||||
|
||||
const beforeConfirmPayment = async (): Promise<boolean> => {
|
||||
setIsSubmitting(true)
|
||||
let token = captchaToken
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
const captchaResponse = await captchaRef.current?.execute({ async: true })
|
||||
token = captchaResponse?.response ?? null
|
||||
setCaptchaToken(token)
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSubmitting(false)
|
||||
return false
|
||||
}
|
||||
|
||||
setIsSubmitting(false)
|
||||
return true
|
||||
}
|
||||
|
||||
const onConfirmPayment = async () => {
|
||||
const payload = formSubscriptionUpdatePayload(
|
||||
currentSubscription,
|
||||
@@ -155,11 +184,12 @@ const ProUpgrade: FC<Props> = ({
|
||||
selectedAddons,
|
||||
nonChangeableAddons,
|
||||
selectedPaymentMethodId,
|
||||
projectRegion
|
||||
projectRegion,
|
||||
captchaToken ?? undefined
|
||||
)
|
||||
|
||||
setIsSubmitting(true)
|
||||
const res = await patch(`${API_URL}/projects/${projectRef}/subscription`, payload)
|
||||
resetCaptcha()
|
||||
|
||||
if (res?.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
@@ -199,17 +229,17 @@ const ProUpgrade: FC<Props> = ({
|
||||
enter="transition ease-out duration-300"
|
||||
enterFrom="transform opacity-0 translate-x-10"
|
||||
enterTo="transform opacity-100 translate-x-0"
|
||||
className="flex w-full items-start justify-between"
|
||||
className="flex items-start justify-between w-full"
|
||||
>
|
||||
<div className="flex-grow mt-10">
|
||||
<div className="relative space-y-4">
|
||||
<div className="space-y-4 2xl:min-w-5xl mx-auto px-32">
|
||||
<div className="px-32 mx-auto space-y-4 2xl:min-w-5xl">
|
||||
<BackButton onClick={() => onSelectBack()} />
|
||||
<h4 className="text-lg text-scale-900 !mb-8">Change your project's subscription</h4>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="space-y-8 overflow-y-auto pb-8 2xl:min-w-5xl mx-auto px-32"
|
||||
className="px-32 pb-8 mx-auto space-y-8 overflow-y-auto 2xl:min-w-5xl"
|
||||
style={{ height: 'calc(100vh - 9rem - 57px)' }}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
@@ -233,14 +263,14 @@ const ProUpgrade: FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-16 rounded border border-panel-border-light border-panel-border-dark bg-panel-body-light px-6 py-4 drop-shadow-sm dark:bg-panel-body-dark">
|
||||
<div className="flex items-center justify-between gap-16 px-6 py-4 border rounded border-panel-border-light border-panel-border-dark bg-panel-body-light drop-shadow-sm dark:bg-panel-body-dark">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>Enable spend cap</p>
|
||||
<IconHelpCircle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="cursor-pointer opacity-50 transition hover:opacity-100"
|
||||
className="transition opacity-50 cursor-pointer hover:opacity-100"
|
||||
onClick={() => setShowSpendCapHelperModal(true)}
|
||||
/>
|
||||
</div>
|
||||
@@ -318,8 +348,22 @@ const ProUpgrade: FC<Props> = ({
|
||||
onSelectAddNewPaymentMethod={() => {
|
||||
setShowAddPaymentMethodModal(true)
|
||||
}}
|
||||
beforeConfirmPayment={beforeConfirmPayment}
|
||||
onConfirmPayment={onConfirmPayment}
|
||||
isSubmitting={isSubmitting}
|
||||
captcha={
|
||||
<HCaptcha
|
||||
ref={captchaRef}
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
size="invisible"
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token)
|
||||
}}
|
||||
onExpire={() => {
|
||||
setCaptchaToken(null)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -338,7 +382,7 @@ const ProUpgrade: FC<Props> = ({
|
||||
header="Enabling spend cap"
|
||||
onCancel={() => setShowSpendCapHelperModal(false)}
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="py-4 space-y-4">
|
||||
<Modal.Content>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm">
|
||||
@@ -353,7 +397,7 @@ const ProUpgrade: FC<Props> = ({
|
||||
</p>
|
||||
{/* Maybe instead of a table, show something more interactive like a spend cap playground */}
|
||||
{/* Maybe ideate this in Figma first but this is good enough for now */}
|
||||
<div className="rounded border border-scale-600 bg-scale-500">
|
||||
<div className="border rounded border-scale-600 bg-scale-500">
|
||||
<div className="flex items-center px-4 pt-2 pb-1">
|
||||
<p className="w-[50%] text-sm text-scale-1100">Item</p>
|
||||
<p className="w-[25%] text-sm text-scale-1100">Limit</p>
|
||||
@@ -380,7 +424,7 @@ const ProUpgrade: FC<Props> = ({
|
||||
<IconHelpCircle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="cursor-pointer opacity-50 transition hover:opacity-100"
|
||||
className="transition opacity-50 cursor-pointer hover:opacity-100"
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom">
|
||||
|
||||
@@ -812,7 +812,7 @@ create publication supabase_realtime
|
||||
sql: `
|
||||
-- Create a table for public profiles
|
||||
create table profiles (
|
||||
id uuid references auth.users not null primary key,
|
||||
id uuid references auth.users on delete cascade not null primary key,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
full_name text,
|
||||
|
||||
@@ -122,8 +122,10 @@ const BucketRow = ({
|
||||
<ProductMenuItem
|
||||
key={bucket.id}
|
||||
name={
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>{bucket.name}</p>
|
||||
<div className="flex items-center justify-between space-x-2 truncate w-full">
|
||||
<p className="truncate" title={bucket.name}>
|
||||
{bucket.name}
|
||||
</p>
|
||||
{bucket.public && <Badge color="yellow">Public</Badge>}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const HeaderPathEdit = ({ loading, breadcrumbs, togglePathEdit }) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex cursor-pointer items-center">
|
||||
<p className="ml-3 text-sm">{breadcrumbs[breadcrumbs.length - 1] || ''}</p>
|
||||
<p className="ml-3 text-sm truncate">{breadcrumbs[breadcrumbs.length - 1] || ''}</p>
|
||||
<div className="ml-3 flex items-center space-x-2 opacity-0 transition group-hover:opacity-100">
|
||||
<Button type="text" icon={<IconEdit2 />}>
|
||||
Navigate
|
||||
@@ -224,6 +224,7 @@ const FileExplorerHeader = ({
|
||||
autoFocus
|
||||
key="pathSet"
|
||||
type="text"
|
||||
size="small"
|
||||
value={pathString}
|
||||
onChange={onUpdatePathString}
|
||||
placeholder="e.g Parent Folder/Child Folder"
|
||||
|
||||
@@ -31,16 +31,16 @@ const ProductMenuItem: FC<Props> = ({
|
||||
}) => {
|
||||
const menuItem = (
|
||||
<Menu.Item icon={icon} rounded active={isActive} onClick={onClick}>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span
|
||||
<div className="flex w-full items-center justify-between gap-1">
|
||||
<div
|
||||
title={hoverText ? hoverText : typeof name === 'string' ? name : ''}
|
||||
className={'flex items-center gap-2 truncate ' + textClassName}
|
||||
className={'flex items-center gap-2 truncate w-full ' + textClassName}
|
||||
>
|
||||
{name}{' '}
|
||||
{label !== undefined && (
|
||||
<span className="text-orange-800 text-xs font-normal">{label}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"uniforms-bootstrap4": "^3.6.2",
|
||||
"uniforms-bridge-json-schema": "^3.6.2",
|
||||
"uuid": "^8.3.2",
|
||||
"valtio": "^1.7.6",
|
||||
"yup": "^0.32.11",
|
||||
"yup-password": "^0.2.2",
|
||||
"zxcvbn": "^4.4.2"
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useEffect, useState } from 'react'
|
||||
import { Hydrate, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
import { RootStore } from 'stores'
|
||||
import HCaptchaLoadedStore from 'stores/hcaptcha-loaded-store'
|
||||
import { StoreProvider } from 'hooks'
|
||||
import { GOTRUE_ERRORS } from 'lib/constants'
|
||||
import { auth } from 'lib/gotrue'
|
||||
@@ -115,6 +116,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
</RouteValidationWrapper>
|
||||
</PageTelemetry>
|
||||
|
||||
<HCaptchaLoadedStore />
|
||||
<PortalToast />
|
||||
<ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
|
||||
</FlagProvider>
|
||||
|
||||
@@ -80,7 +80,7 @@ const BillingUpdateFree: NextPageWithLayout = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mx-auto my-10 max-w-5xl">
|
||||
<div className="mx-auto my-10 max-w-5xl px-32 xl:px-0">
|
||||
<ExitSurvey
|
||||
freeTier={freeTier}
|
||||
subscription={subscription}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
|
||||
const hCaptchaLoadedStoreState = proxy({
|
||||
loaded: false,
|
||||
setLoaded() {
|
||||
hCaptchaLoadedStoreState.loaded = true
|
||||
},
|
||||
})
|
||||
|
||||
export const useIsHCaptchaLoaded = () => {
|
||||
const snap = useSnapshot(hCaptchaLoadedStoreState)
|
||||
|
||||
return snap.loaded
|
||||
}
|
||||
|
||||
const HCaptchaLoadedStore = () => {
|
||||
const onLoad = () => {
|
||||
hCaptchaLoadedStoreState.setLoaded()
|
||||
}
|
||||
|
||||
return (
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
size="invisible"
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default HCaptchaLoadedStore
|
||||
@@ -154,7 +154,6 @@ export default class MetaStore implements IMetaStore {
|
||||
'extensions',
|
||||
'information_schema',
|
||||
'net',
|
||||
'pg_catalog',
|
||||
'pgsodium',
|
||||
'pgsodium_masks',
|
||||
'pgbouncer',
|
||||
|
||||
@@ -27,6 +27,14 @@ export default class SchemaStore extends PostgresMetaInterface<PostgresSchema> {
|
||||
this.views = []
|
||||
}
|
||||
|
||||
// Dashboard to hide schemas with pg_ prefix
|
||||
list(filter: any) {
|
||||
const schemasFilter = (schema: PostgresSchema) =>
|
||||
!schema.name.startsWith('pg_') && (typeof filter === 'function' ? filter(schema) : true)
|
||||
|
||||
return super.list(schemasFilter)
|
||||
}
|
||||
|
||||
async loadViews(schema: string) {
|
||||
const query = `
|
||||
select schemaname as schema, viewname as name from pg_catalog.pg_views
|
||||
|
||||
@@ -20,8 +20,8 @@ export default class TriggersStore extends PostgresMetaInterface<any> {
|
||||
*/
|
||||
list(filter: any) {
|
||||
const triggersFilter = (h: any) =>
|
||||
h.function_schema != 'supabase_functions' &&
|
||||
h.schema != 'net' &&
|
||||
h.function_schema !== 'supabase_functions' &&
|
||||
h.schema !== 'net' &&
|
||||
(typeof filter === 'function' ? filter(h) : true)
|
||||
|
||||
return super.list(triggersFilter)
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
"version": "1.0.0",
|
||||
"description": "These tests can be run with Docker.",
|
||||
"scripts": {
|
||||
"docker:up": "cd ../docker && ENABLE_EMAIL_AUTOCONFIRM=true docker-compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml up -d",
|
||||
"docker:down": "cd ../docker && docker-compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml down",
|
||||
"docker:up": "cd ../docker && ENABLE_EMAIL_AUTOCONFIRM=true docker compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml up -d",
|
||||
"docker:down": "cd ../docker && docker compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml down",
|
||||
"test": "jest",
|
||||
"test:local": "jest --testNamePattern '^((?!\\[skip-local\\]).)*$' --testPathPattern '^((?!realtime).)*$' --detectOpenHandles",
|
||||
"test:stage": "jest --testNamePattern '^((?!\\[skip-stage\\]).)*$' --detectOpenHandles",
|
||||
|
||||
Reference in new issue
Block a user