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

This commit is contained in:
Terry Sutton committed 2022-12-21 11:36:23 -03:30
commit ffcaff9aee
41 files changed
+610 -404

No files matched your search

+2 -2
View File
@@ -14,7 +14,7 @@ const Footer = () => {
<span className="text-xs text-scale-900">Supabase 2022</span>
<span className="text-xs text-scale-900">—</span>
{footerData.map((item) => (
<Link href={item.url}>
<Link href={item.url} key={item.url}>
<a className="text-xs text-scale-800 hover:underline">{item.title}</a>
</Link>
))}
@@ -41,7 +41,7 @@ const Footer = () => {
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16">
<path
fill="none"
fill-rule="nonzero"
fillRule="nonzero"
stroke={`${isDarkMode ? '#fff' : '#333'}`}
d="M16.238 2.16a14.628 14.628 0 0 0-3.664-1.159.055.055 0 0 0-.059.028c-.158.288-.333.662-.456.957a13.432 13.432 0 0 0-4.115 0 9.76 9.76 0 0 0-.464-.957.057.057 0 0 0-.058-.028 14.587 14.587 0 0 0-3.664 1.16.053.053 0 0 0-.024.02C1.4 5.74.76 9.21 1.074 12.637c.002.017.011.033.024.043a14.813 14.813 0 0 0 4.494 2.318.058.058 0 0 0 .064-.02c.346-.483.654-.992.92-1.527a.059.059 0 0 0-.032-.08c-.49-.19-.956-.421-1.404-.684a.06.06 0 0 1-.006-.097c.094-.072.189-.148.279-.223a.055.055 0 0 1 .058-.008c2.946 1.372 6.135 1.372 9.046 0a.055.055 0 0 1 .06.007 7.2 7.2 0 0 0 .279.224.06.06 0 0 1-.005.097 9.143 9.143 0 0 1-1.405.683.06.06 0 0 0-.03.081c.27.534.578 1.043.918 1.526.014.02.04.029.063.021A14.763 14.763 0 0 0 18.9 12.68a.06.06 0 0 0 .023-.042c.376-3.962-.628-7.404-2.66-10.455a.046.046 0 0 0-.024-.021Zm-9.223 8.39c-.887 0-1.618-.831-1.618-1.852 0-1.02.717-1.851 1.618-1.851.908 0 1.632.838 1.618 1.851 0 1.02-.717 1.851-1.618 1.851Zm5.981 0c-.887 0-1.618-.831-1.618-1.852 0-1.02.717-1.851 1.618-1.851.908 0 1.632.838 1.618 1.851 0 1.02-.71 1.851-1.618 1.851Z"
/>
@@ -129,7 +129,7 @@ const NavigationMenuHome = () => {
<ul className="relative w-full flex flex-col gap-4">
{home.map((section, sectionIndex) => {
return (
<>
<div key={`section-container-${sectionIndex}-border`}>
{sectionIndex !== 0 && (
<div
className="h-px w-full bg-blackA-300 dark:bg-whiteA-300"
@@ -176,7 +176,7 @@ const NavigationMenuHome = () => {
})}
</div>
</div>
</>
</div>
)
})}
</ul>
@@ -173,7 +173,7 @@ const SideNav = () => {
<NavigationMenuGuideList id={'reference'} />
{/* // Client Libs */}
<NavigationMenuRefList
key={'reference-js-menu'}
key={'reference-js-menu-v1'}
id={'reference_javascript_v1'}
commonSections={libCommonSections}
lib="javascript"
@@ -194,7 +194,7 @@ const SideNav = () => {
allowedClientKeys={generateAllowedClientLibKeys(libCommonSections, spec_dart_v0)}
/>
<NavigationMenuRefList
key={'reference-dart-menu'}
key={'reference-dart-menu-v1'}
id={'reference_dart_v1'}
commonSections={libCommonSections}
lib="dart"
+18 -12
View File
@@ -31,10 +31,11 @@ const Layout: FC<Props> = (props) => {
const [tocList, setTocList] = useState([])
const { asPath } = useRouter()
const page = getPageType(asPath)
const router = useRouter()
const EDIT_BUTTON_EXCLUDE_LIST = ['/404']
useEffect(() => {
if (hash && tocList.length > 0) {
highlightSelectedTocItem(hash as string)
@@ -100,18 +101,23 @@ const Layout: FC<Props> = (props) => {
)}
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-8"></div>
<MDXProvider components={components}>{props.children}</MDXProvider>
<div className="mt-16 not-prose">
<div>
<Link
href={`https://github.com/supabase/supabase/edit/master/apps/docs/pages${router.asPath}.mdx`}
passHref
>
<a className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200">
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
</a>
</Link>
{EDIT_BUTTON_EXCLUDE_LIST.includes(router.route) ? (
<></>
) : (
<div className="mt-16 not-prose">
<div>
<Link
href={`https://github.com/supabase/supabase/edit/master/apps/docs/pages${router.asPath}.mdx`}
passHref
>
<a className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200">
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
</a>
</Link>
</div>
</div>
</div>
)}
</article>
</div>
{!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && (
+6 -1
View File
@@ -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,
-52
View File
@@ -1,52 +0,0 @@
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { Button } from 'ui'
import Head from 'next/head'
export default function Custom404() {
const [show404, setShow404] = useState<boolean>(false)
useEffect(() => {
setTimeout(() => {
setShow404(true)
}, 500)
}, [])
return (
<>
<Head>
<title>404 | Supabase</title>
</Head>
<div className="relative mx-auto flex h-screen w-full flex-col items-center justify-center">
<div className="absolute">
<h1
className={`text-scale-1200 select-none text-[14rem] opacity-[5%] filter transition duration-200 sm:text-[18rem] lg:text-[28rem] ${
show404 ? 'blur-sm' : 'blur-none'
}`}
>
404
</h1>
</div>
<div
className={`flex flex-col items-center justify-center space-y-6 transition ${
show404 ? 'opacity-100' : 'opacity-0'
}`}
>
<div className="text-scale-1200 flex w-[320px] flex-col items-center justify-center space-y-3">
<h1 className="m-2 text-2xl">Looking for something?</h1>
<p className="text-center text-sm">
We could not find the page that you are looking for!
</p>
</div>
<div className="flex items-center space-x-4">
<Link href="/">
<Button as="a" size="small" className="text-white">
Head back
</Button>
</Link>
</div>
</div>
</div>
</>
)
}
+1 -1
View File
@@ -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)).
+154 -186
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -30,7 +30,7 @@ export const meta = {
">
<div className="col-span-12 lg:col-span-7 flex flex-col gap-6">
<img className="hidden lg:visible absolute right-0 top-0 w-[500px]" src="/docs/img/cards/dark.png" />
<div classNam="">
<div className="">
<h3 className="text-2xl m-0 mb-3">Getting Started</h3>
Discover how to set up a database to an app making queries in just a few minutes.
+2 -1
View File
@@ -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`:
![This image shows in middleware architecture. We used docker-compose to pull up 5 open source tools: Kong, Realtime, PostgREST, PG-API, and PG-BOSS.](/images/blog/supabase-middleware-docker.png)
![This image shows in middleware architecture. We used docker compose to pull up 5 open source tools: Kong, Realtime, PostgREST, PG-API, and PG-BOSS.](/images/blog/supabase-middleware-docker.png)
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/).
+1 -1
View File
@@ -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.
+5 -11
View File
@@ -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"
}
]
-10
View File
@@ -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',
+1 -6
View File
@@ -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 ">
+4 -4
View File
@@ -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"
+61
View File
@@ -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
View File
@@ -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/",
@@ -135,13 +135,6 @@
"summary": "Retrieve object info",
"type": "operation"
},
{
"id": "retrieve-object-info",
"title": "Retrieve object info",
"slug": "retrieve-object-info",
"summary": "Retrieve object info",
"type": "operation"
},
{
"id": "copies-an-object",
"title": "Copies an object",
+2 -2
View File
@@ -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.
@@ -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>
+1
View File
@@ -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"
+2
View File
@@ -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}
+31
View File
@@ -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
-1
View File
@@ -154,7 +154,6 @@ export default class MetaStore implements IMetaStore {
'extensions',
'information_schema',
'net',
'pg_catalog',
'pgsodium',
'pgsodium_masks',
'pgbouncer',
+8
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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",