From f9a41816c75ceb9ae44e02cee54195276dd2c5d0 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 5 Oct 2022 15:58:21 +1100 Subject: [PATCH 01/86] adds projects page --- .../layouts/AccountLayout/AccountLayout.tsx | 4 +- studio/pages/projects.tsx | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 studio/pages/projects.tsx diff --git a/studio/components/layouts/AccountLayout/AccountLayout.tsx b/studio/components/layouts/AccountLayout/AccountLayout.tsx index 24dfbbaa732..439aa7efbd8 100644 --- a/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -47,9 +47,9 @@ const AccountLayout: FC = ({ children, title, breadcrumbs }) => { key: 'projects', links: [ { - isActive: router.pathname === '/', + isActive: router.pathname === '/projects', label: 'All projects', - href: '/', + href: '/projects', key: 'all-projects-item', }, ], diff --git a/studio/pages/projects.tsx b/studio/pages/projects.tsx new file mode 100644 index 00000000000..217944485d8 --- /dev/null +++ b/studio/pages/projects.tsx @@ -0,0 +1,53 @@ +import ProjectList from 'components/interfaces/Home/ProjectList' +import { AccountLayout } from 'components/layouts' +import OrganizationDropdown from 'components/to-be-cleaned/Dropdown/OrganizationDropdown' +import Connecting from 'components/ui/Loading/Loading' +import { useStore } from 'hooks' +import { IS_PLATFORM } from 'lib/constants' +import { observer } from 'mobx-react-lite' +import { NextPageWithLayout } from 'types' + +const ProjectsPage: NextPageWithLayout = () => { + const { app } = useStore() + + return ( + <> + {app.organizations.isLoading ? ( +
+ +
+ ) : ( +
+ {IS_PLATFORM && ( +
+
+
+ +
+
+
+ )} +
+ +
+
+ )} + + ) +} + +ProjectsPage.getLayout = (page) => ( + + {page} + +) + +export default observer(ProjectsPage) From 287c05396da201677c10c89a36f25feca7f80366 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 7 Oct 2022 20:52:23 +1100 Subject: [PATCH 02/86] adds login screen scaffolds --- .../interfaces/Login/ForgotPasswordForm.tsx | 5 + .../components/interfaces/Login/LoginForm.tsx | 71 ++++++++++ .../interfaces/Login/LoginWithGitHub.tsx | 26 ++++ .../interfaces/Login/RegisterForm.tsx | 86 ++++++++++++ .../interfaces/Login/ResetPasswordForm.tsx | 5 + .../layouts/LoginLayout/LoginLayout.tsx | 82 +++++++++++ studio/components/layouts/index.ts | 2 + studio/pages/forgot-password.tsx | 5 + studio/pages/index.tsx | 127 +++++++++--------- studio/pages/register.tsx | 39 ++++++ studio/pages/reset-password.tsx | 5 + 11 files changed, 391 insertions(+), 62 deletions(-) create mode 100644 studio/components/interfaces/Login/ForgotPasswordForm.tsx create mode 100644 studio/components/interfaces/Login/LoginForm.tsx create mode 100644 studio/components/interfaces/Login/LoginWithGitHub.tsx create mode 100644 studio/components/interfaces/Login/RegisterForm.tsx create mode 100644 studio/components/interfaces/Login/ResetPasswordForm.tsx create mode 100644 studio/components/layouts/LoginLayout/LoginLayout.tsx create mode 100644 studio/pages/forgot-password.tsx create mode 100644 studio/pages/register.tsx create mode 100644 studio/pages/reset-password.tsx diff --git a/studio/components/interfaces/Login/ForgotPasswordForm.tsx b/studio/components/interfaces/Login/ForgotPasswordForm.tsx new file mode 100644 index 00000000000..becbd6b0f6a --- /dev/null +++ b/studio/components/interfaces/Login/ForgotPasswordForm.tsx @@ -0,0 +1,5 @@ +const ForgotPasswordForm = () => { + return
+} + +export default ForgotPasswordForm diff --git a/studio/components/interfaces/Login/LoginForm.tsx b/studio/components/interfaces/Login/LoginForm.tsx new file mode 100644 index 00000000000..c0af5e994d9 --- /dev/null +++ b/studio/components/interfaces/Login/LoginForm.tsx @@ -0,0 +1,71 @@ +import { auth } from 'lib/gotrue' +import Link from 'next/link' +import { Button, Form, Input } from 'ui' +import { object, string } from 'yup' + +const loginSchema = object({ + email: string().email('Must be a valid email').required('Email is required'), + password: string().required('Password is required'), +}) + +const LoginForm = () => { + const onLogin = async ({ email, password }: { email: string; password: string }) => { + const result = await auth.signInWithPassword({ email, password }) + console.log('result:', result) + } + + return ( +
+

Login

+ +
+ {({ isSubmitting }: { isSubmitting: boolean }) => { + return ( +
+ + + + +
+ + +
+ ) + }} + + + + Forgot Password + +
+ ) +} + +export default LoginForm diff --git a/studio/components/interfaces/Login/LoginWithGitHub.tsx b/studio/components/interfaces/Login/LoginWithGitHub.tsx new file mode 100644 index 00000000000..2cf9cdf5760 --- /dev/null +++ b/studio/components/interfaces/Login/LoginWithGitHub.tsx @@ -0,0 +1,26 @@ +import { auth } from 'lib/gotrue' +import { Button, IconGitHub } from 'ui' + +const LoginWithGitHub = () => { + async function handleGithubSignIn() { + try { + const { error } = await auth.signInWithOAuth({ + provider: 'github', + options: { + redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/projects?auth=true`, + }, + }) + if (error) throw error + } catch (error) { + console.error(error) + } + } + + return ( + + ) +} + +export default LoginWithGitHub diff --git a/studio/components/interfaces/Login/RegisterForm.tsx b/studio/components/interfaces/Login/RegisterForm.tsx new file mode 100644 index 00000000000..bff2d69c502 --- /dev/null +++ b/studio/components/interfaces/Login/RegisterForm.tsx @@ -0,0 +1,86 @@ +import { auth } from 'lib/gotrue' +import Link from 'next/link' +import { Button, Form, IconLock, Input } from 'ui' +import { object, string } from 'yup' + +const registerSchema = object({ + username: string() + .max(39, 'Username is too long (maximum is 39 characters)') + .matches( + // Username regex is 'borrowed' from GitHub + /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i, + 'Username may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen' + ) + .required('Username is required'), + email: string().email('Must be a valid email').required('Email is required'), + password: string().required('Password is required'), +}) + +const RegisterForm = () => { + const onRegister = async ({ + username, + email, + password, + }: { + username: string + email: string + password: string + }) => { + const result = await auth.signUp({ email, password, options: { data: { username } } }) + console.log('result:', result) + } + + return ( +
+ {({ isSubmitting }: { isSubmitting: boolean }) => { + return ( +
+ + + + + + + +
+ ) + }} +
+ ) +} + +export default RegisterForm diff --git a/studio/components/interfaces/Login/ResetPasswordForm.tsx b/studio/components/interfaces/Login/ResetPasswordForm.tsx new file mode 100644 index 00000000000..5b34602b0f5 --- /dev/null +++ b/studio/components/interfaces/Login/ResetPasswordForm.tsx @@ -0,0 +1,5 @@ +const ResetPasswordForm = () => { + return
+} + +export default ResetPasswordForm diff --git a/studio/components/layouts/LoginLayout/LoginLayout.tsx b/studio/components/layouts/LoginLayout/LoginLayout.tsx new file mode 100644 index 00000000000..25f1cf7aa1d --- /dev/null +++ b/studio/components/layouts/LoginLayout/LoginLayout.tsx @@ -0,0 +1,82 @@ +import { useStore } from 'hooks' +import { observer } from 'mobx-react-lite' +import Image from 'next/image' +import Link from 'next/link' +import { PropsWithChildren } from 'react' + +type LoginLayoutProps = { + title: string + showDisclaimer?: boolean +} + +const LoginLayout = ({ + title, + showDisclaimer = true, + children, +}: PropsWithChildren) => { + const { + ui: { theme }, + } = useStore() + + return ( +
+
+ +
+ +

{title}

+ +
+
+ Supabase Logo + + {children} +
+
+ + {showDisclaimer && ( +
+

+ By continuing, you agree to Supabase's{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Policy + + , and to receive periodic emails with updates. +

+
+ )} +
+ ) +} + +export default observer(LoginLayout) diff --git a/studio/components/layouts/index.ts b/studio/components/layouts/index.ts index a7cd8c77950..2457dda09a7 100644 --- a/studio/components/layouts/index.ts +++ b/studio/components/layouts/index.ts @@ -13,6 +13,7 @@ import { WizardLayoutWithoutAuth } from './WizardLayout' import VercelIntegrationLayout from './VercelIntegrationLayout' import BillingLayout from './BillingLayout' import LogsExplorerLayout from './LogsExplorerLayout/LogsExplorerLayout' +import LoginLayout from './LoginLayout/LoginLayout' export { ProjectLayoutWithAuth, @@ -30,6 +31,7 @@ export { VercelIntegrationLayout, BillingLayout, LogsExplorerLayout, + LoginLayout, } export default ProjectLayout diff --git a/studio/pages/forgot-password.tsx b/studio/pages/forgot-password.tsx new file mode 100644 index 00000000000..2be3114a874 --- /dev/null +++ b/studio/pages/forgot-password.tsx @@ -0,0 +1,5 @@ +const ForgotPasswordPage = () => { + return
+} + +export default ForgotPasswordPage diff --git a/studio/pages/index.tsx b/studio/pages/index.tsx index c85615379e9..7599420efda 100644 --- a/studio/pages/index.tsx +++ b/studio/pages/index.tsx @@ -1,50 +1,58 @@ -import { useEffect } from 'react' import { observer } from 'mobx-react-lite' import { NextRouter, useRouter } from 'next/router' - -import { NextPageWithLayout } from 'types' -import { useProfile, useStore, withAuth } from 'hooks' +import { useEffect } from 'react' +import { withAuth } from 'hooks' import { auth } from 'lib/gotrue' -import { IS_PLATFORM } from 'lib/constants' - -import Connecting from 'components/ui/Loading' -import { AccountLayoutWithoutAuth } from 'components/layouts' +import { NextPageWithLayout } from 'types' import Landing from 'components/interfaces/Home/Landing' -import ProjectList from 'components/interfaces/Home/ProjectList' -import OrganizationDropdown from 'components/to-be-cleaned/Dropdown/OrganizationDropdown' - -const Home: NextPageWithLayout = () => { - const { app } = useStore() +import LoginForm from 'components/interfaces/Login/LoginForm' +import LoginWithGitHub from 'components/interfaces/Login/LoginWithGitHub' +import { AccountLayoutWithoutAuth, LoginLayout } from 'components/layouts' +import Connecting from 'components/ui/Loading' +import Link from 'next/link' +const LoginPage: NextPageWithLayout = () => { return ( <> - {app.organizations.isLoading ? ( -
- -
- ) : ( -
- {IS_PLATFORM && ( -
-
-
- -
-
-
- )} -
- +
+ + +
+
+
+
+
+ Or
- )} + + +
+ +
+ +
+
+ Need an account?{' '} + + Register + +
+ +
+ Forgot your password?{' '} + + Reset Password + +
+
) } -Home.getLayout = (page) => {page} +LoginPage.getLayout = (page) => {page} -export default observer(Home) +export default LoginPage // detect for redirect from 3rd party service like vercel, aws... const isRedirectFromThirdPartyService = (router: NextRouter) => router.query.next !== undefined @@ -73,37 +81,32 @@ const UnauthorizedLanding = () => { const IndexLayout = withAuth( observer(({ children }) => { const router = useRouter() - const { profile, isLoading } = useProfile() - if (isLoading) { - return - } + // if (!profile) { + // return + // } else { + // const isRedirect = isRedirectFromThirdPartyService(router) + // if (isRedirect) { + // const queryParams = (router.query as any) || {} + // const params = new URLSearchParams(queryParams) + // if (router.query?.next?.includes('https://vercel.com')) { + // router.push(`/vercel/integrate?${params.toString()}`) + // } else if (router.query?.next?.includes('new-project')) { + // router.push('/new/project') + // } else if (router.query?.next?.includes('join')) { + // router.push(`/${router.query.next}`) + // } else if ( + // typeof router.query?.next === 'string' && + // router.query?.next?.startsWith('project/_/') + // ) { + // router.push(router.query.next as string) + // } else { + // router.push('/') + // } - if (!profile) { - return - } else { - const isRedirect = isRedirectFromThirdPartyService(router) - if (isRedirect) { - const queryParams = (router.query as any) || {} - const params = new URLSearchParams(queryParams) - if (router.query?.next?.includes('https://vercel.com')) { - router.push(`/vercel/integrate?${params.toString()}`) - } else if (router.query?.next?.includes('new-project')) { - router.push('/new/project') - } else if (router.query?.next?.includes('join')) { - router.push(`/${router.query.next}`) - } else if ( - typeof router.query?.next === 'string' && - router.query?.next?.startsWith('project/_/') - ) { - router.push(router.query.next as string) - } else { - router.push('/') - } - - return - } - } + // return + // } + // } return ( { + return ( + <> +
+ + +
+
+
+
+
+ Or +
+
+ + +
+ +
+ +
+ Already have an account?{' '} + + Login + +
+ + ) +} + +RegisterPage.getLayout = (page) => {page} + +export default RegisterPage diff --git a/studio/pages/reset-password.tsx b/studio/pages/reset-password.tsx new file mode 100644 index 00000000000..80827264acf --- /dev/null +++ b/studio/pages/reset-password.tsx @@ -0,0 +1,5 @@ +const ResetPasswordPage = () => { + return
+} + +export default ResetPasswordPage From c81a72f666d32b602380223fa736b14a9533c616 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Mon, 10 Oct 2022 20:48:44 +1100 Subject: [PATCH 03/86] adds forgot password page --- .../interfaces/Login/ForgotPasswordForm.tsx | 50 ++++++++++- .../components/interfaces/Login/LoginForm.tsx | 87 +++++++++---------- studio/pages/forgot-password.tsx | 30 ++++++- 3 files changed, 116 insertions(+), 51 deletions(-) diff --git a/studio/components/interfaces/Login/ForgotPasswordForm.tsx b/studio/components/interfaces/Login/ForgotPasswordForm.tsx index becbd6b0f6a..272cdb60671 100644 --- a/studio/components/interfaces/Login/ForgotPasswordForm.tsx +++ b/studio/components/interfaces/Login/ForgotPasswordForm.tsx @@ -1,5 +1,53 @@ +import { auth } from 'lib/gotrue' +import { Button, Form, Input } from 'ui' +import { object, string } from 'yup' + +const forgotPasswordSchema = object({ + email: string().email('Must be a valid email').required('Email is required'), +}) + const ForgotPasswordForm = () => { - return
+ const onForgotPassword = async ({ email }: { email: string }) => { + const result = await auth.resetPasswordForEmail(email) + console.log('result:', result) + } + + return ( +
+ {({ isSubmitting }: { isSubmitting: boolean }) => { + return ( +
+ + +
+ + +
+ ) + }} + + ) } export default ForgotPasswordForm diff --git a/studio/components/interfaces/Login/LoginForm.tsx b/studio/components/interfaces/Login/LoginForm.tsx index c0af5e994d9..39dcbd416bd 100644 --- a/studio/components/interfaces/Login/LoginForm.tsx +++ b/studio/components/interfaces/Login/LoginForm.tsx @@ -1,5 +1,4 @@ import { auth } from 'lib/gotrue' -import Link from 'next/link' import { Button, Form, Input } from 'ui' import { object, string } from 'yup' @@ -15,56 +14,48 @@ const LoginForm = () => { } return ( -
-

Login

+
+ {({ isSubmitting }: { isSubmitting: boolean }) => { + return ( +
+ - - {({ isSubmitting }: { isSubmitting: boolean }) => { - return ( -
- + - +
-
- - -
- ) - }} - - - - Forgot Password - -
+ +
+ ) + }} + ) } diff --git a/studio/pages/forgot-password.tsx b/studio/pages/forgot-password.tsx index 2be3114a874..ff5f6bbeed5 100644 --- a/studio/pages/forgot-password.tsx +++ b/studio/pages/forgot-password.tsx @@ -1,5 +1,31 @@ -const ForgotPasswordPage = () => { - return
+import ForgotPasswordForm from 'components/interfaces/Login/ForgotPasswordForm' +import { LoginLayout } from 'components/layouts' +import Link from 'next/link' +import { NextPageWithLayout } from 'types' + +const ForgotPasswordPage: NextPageWithLayout = () => { + return ( + <> +
+ +
+ +
+ +
+ Already have an account?{' '} + + Login + +
+ + ) } +ForgotPasswordPage.getLayout = (page) => ( + + {page} + +) + export default ForgotPasswordPage From 6db7f5ada8159598edc6cc276de06f40c5deed79 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Mon, 10 Oct 2022 21:44:56 +1100 Subject: [PATCH 04/86] adds reset password form and toasts --- .../interfaces/Login/ForgotPasswordForm.tsx | 31 +++++++- .../components/interfaces/Login/LoginForm.tsx | 35 ++++++++- .../interfaces/Login/RegisterForm.tsx | 34 +++++++- .../interfaces/Login/ResetPasswordForm.tsx | 77 ++++++++++++++++++- .../layouts/LoginLayout/LoginLayout.tsx | 20 +++-- studio/pages/index.tsx | 6 +- studio/pages/reset-password.tsx | 30 +++++++- 7 files changed, 212 insertions(+), 21 deletions(-) diff --git a/studio/components/interfaces/Login/ForgotPasswordForm.tsx b/studio/components/interfaces/Login/ForgotPasswordForm.tsx index 272cdb60671..770c356b78b 100644 --- a/studio/components/interfaces/Login/ForgotPasswordForm.tsx +++ b/studio/components/interfaces/Login/ForgotPasswordForm.tsx @@ -1,4 +1,6 @@ +import { useStore } from 'hooks' import { auth } from 'lib/gotrue' +import { useRouter } from 'next/router' import { Button, Form, Input } from 'ui' import { object, string } from 'yup' @@ -7,16 +9,39 @@ const forgotPasswordSchema = object({ }) const ForgotPasswordForm = () => { + const { ui } = useStore() + const router = useRouter() + const onForgotPassword = async ({ email }: { email: string }) => { - const result = await auth.resetPasswordForEmail(email) - console.log('result:', result) + const toastId = ui.setNotification({ + category: 'loading', + message: `Sending password reset email...`, + }) + + const { error } = await auth.resetPasswordForEmail(email) + + if (!error) { + ui.setNotification({ + id: toastId, + category: 'success', + message: `Password reset email sent successfully!`, + }) + + await router.push('/') + } else { + ui.setNotification({ + id: toastId, + category: 'error', + message: error.message, + }) + } } return (
diff --git a/studio/components/interfaces/Login/LoginForm.tsx b/studio/components/interfaces/Login/LoginForm.tsx index 39dcbd416bd..f3adae920b4 100644 --- a/studio/components/interfaces/Login/LoginForm.tsx +++ b/studio/components/interfaces/Login/LoginForm.tsx @@ -1,4 +1,7 @@ +import { useStore } from 'hooks' import { auth } from 'lib/gotrue' +import { useRouter } from 'next/router' +import { useSWRConfig } from 'swr' import { Button, Form, Input } from 'ui' import { object, string } from 'yup' @@ -8,9 +11,37 @@ const loginSchema = object({ }) const LoginForm = () => { + const { ui } = useStore() + const router = useRouter() + const { cache } = useSWRConfig() + const onLogin = async ({ email, password }: { email: string; password: string }) => { - const result = await auth.signInWithPassword({ email, password }) - console.log('result:', result) + const toastId = ui.setNotification({ + category: 'loading', + message: `Logging in...`, + }) + + const { error } = await auth.signInWithPassword({ email, password }) + + if (!error) { + ui.setNotification({ + id: toastId, + category: 'success', + message: `Logged in successfully!`, + }) + + // .clear() does actually exist on the cache object, but it's not in the types 🤦🏻 + // @ts-ignore + cache.clear() + + await router.push('/projects') + } else { + ui.setNotification({ + id: toastId, + category: 'error', + message: error.message, + }) + } } return ( diff --git a/studio/components/interfaces/Login/RegisterForm.tsx b/studio/components/interfaces/Login/RegisterForm.tsx index bff2d69c502..cc26ff3c1a8 100644 --- a/studio/components/interfaces/Login/RegisterForm.tsx +++ b/studio/components/interfaces/Login/RegisterForm.tsx @@ -1,5 +1,6 @@ +import { useStore } from 'hooks' import { auth } from 'lib/gotrue' -import Link from 'next/link' +import { useRouter } from 'next/router' import { Button, Form, IconLock, Input } from 'ui' import { object, string } from 'yup' @@ -13,10 +14,15 @@ const registerSchema = object({ ) .required('Username is required'), email: string().email('Must be a valid email').required('Email is required'), - password: string().required('Password is required'), + password: string() + .required('Password is required') + .min(8, 'Password must be at least 8 characters long'), }) const RegisterForm = () => { + const { ui } = useStore() + const router = useRouter() + const onRegister = async ({ username, email, @@ -26,8 +32,28 @@ const RegisterForm = () => { email: string password: string }) => { - const result = await auth.signUp({ email, password, options: { data: { username } } }) - console.log('result:', result) + const toastId = ui.setNotification({ + category: 'loading', + message: `Registering...`, + }) + + const { error } = await auth.signUp({ email, password, options: { data: { username } } }) + + if (!error) { + ui.setNotification({ + id: toastId, + category: 'success', + message: `Logged in successfully!`, + }) + + await router.push('/projects') + } else { + ui.setNotification({ + id: toastId, + category: 'error', + message: error.message, + }) + } } return ( diff --git a/studio/components/interfaces/Login/ResetPasswordForm.tsx b/studio/components/interfaces/Login/ResetPasswordForm.tsx index 5b34602b0f5..335ddfd6206 100644 --- a/studio/components/interfaces/Login/ResetPasswordForm.tsx +++ b/studio/components/interfaces/Login/ResetPasswordForm.tsx @@ -1,5 +1,80 @@ +import { useStore } from 'hooks' +import { auth } from 'lib/gotrue' +import { useRouter } from 'next/router' +import { Button, Form, Input } from 'ui' +import { object, string } from 'yup' + +const resetPasswordSchema = object({ + password: string() + .required('Password is required') + .min(8, 'Password must be at least 8 characters long'), +}) + const ResetPasswordForm = () => { - return
+ const { ui } = useStore() + const router = useRouter() + + const onResetPassword = async ({ password }: { password: string }) => { + const toastId = ui.setNotification({ + category: 'loading', + message: `Saving password...`, + }) + + const { error } = await auth.updateUser({ password }) + + if (!error) { + ui.setNotification({ + id: toastId, + category: 'success', + message: `Password saved successfully!`, + }) + + await router.push('/projects') + } else { + ui.setNotification({ + id: toastId, + category: 'error', + message: error.message, + }) + } + } + + return ( + + {({ isSubmitting }: { isSubmitting: boolean }) => { + return ( +
+ + +
+ + +
+ ) + }} + + ) } export default ResetPasswordForm diff --git a/studio/components/layouts/LoginLayout/LoginLayout.tsx b/studio/components/layouts/LoginLayout/LoginLayout.tsx index 25f1cf7aa1d..642ed3c935d 100644 --- a/studio/components/layouts/LoginLayout/LoginLayout.tsx +++ b/studio/components/layouts/LoginLayout/LoginLayout.tsx @@ -7,11 +7,13 @@ import { PropsWithChildren } from 'react' type LoginLayoutProps = { title: string showDisclaimer?: boolean + logoLinkToMarketingSite?: boolean } const LoginLayout = ({ title, showDisclaimer = true, + logoLinkToMarketingSite = false, children, }: PropsWithChildren) => { const { @@ -24,14 +26,16 @@ const LoginLayout = ({