From 779eef0c1f3b112b6af0ceef596e9102428d2efd Mon Sep 17 00:00:00 2001 From: Meng Zhang Date: Thu, 7 Dec 2023 17:56:08 +0800 Subject: [PATCH] feat: add register / signin / signup / logout workflow (#976) * feat: extract signup page * feat: add signin page * feat: implement auto redirecting * fix: show slack dialog after signin * feat: tweak signin page * feat: add basic user panel + signout * [autofix.ci] apply automated fixes * fix: correct page title * fix lint --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../app/(dashboard)/components/sidebar.tsx | 7 +- ee/tabby-ui/app/(dashboard)/page.tsx | 6 +- ee/tabby-ui/app/auth/components/signup.tsx | 34 ------ ee/tabby-ui/app/auth/layout.tsx | 11 ++ ee/tabby-ui/app/auth/page.tsx | 16 --- .../app/auth/signin/components/signin.tsx | 17 +++ .../signin/components/user-signin-form.tsx | 113 ++++++++++++++++++ ee/tabby-ui/app/auth/signin/page.tsx | 10 ++ .../app/auth/signup/components/signup.tsx | 50 ++++++++ .../components/user-register-form.tsx} | 16 ++- ee/tabby-ui/app/auth/signup/page.tsx | 10 ++ .../playground/components/chat-sessions.tsx | 1 + ee/tabby-ui/components/header.tsx | 33 +++++ ee/tabby-ui/components/ui/icons.tsx | 25 +++- ee/tabby-ui/components/user-panel.tsx | 20 ++++ ee/tabby-ui/lib/gql/generates/gql.ts | 16 ++- ee/tabby-ui/lib/gql/generates/graphql.ts | 108 +++++++++++++++-- ee/tabby-ui/lib/tabby/auth.tsx | 13 +- ee/tabby-ui/lib/tabby/gql.ts | 8 +- 19 files changed, 436 insertions(+), 78 deletions(-) delete mode 100644 ee/tabby-ui/app/auth/components/signup.tsx create mode 100644 ee/tabby-ui/app/auth/layout.tsx delete mode 100644 ee/tabby-ui/app/auth/page.tsx create mode 100644 ee/tabby-ui/app/auth/signin/components/signin.tsx create mode 100644 ee/tabby-ui/app/auth/signin/components/user-signin-form.tsx create mode 100644 ee/tabby-ui/app/auth/signin/page.tsx create mode 100644 ee/tabby-ui/app/auth/signup/components/signup.tsx rename ee/tabby-ui/app/auth/{components/user-auth-form.tsx => signup/components/user-register-form.tsx} (90%) create mode 100644 ee/tabby-ui/app/auth/signup/page.tsx create mode 100644 ee/tabby-ui/components/user-panel.tsx diff --git a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx index 310360f..ed273ae 100644 --- a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx +++ b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx @@ -1,5 +1,6 @@ 'use client' +import UserPanel from '@/components/user-panel' import { cn } from '@/lib/utils' import { cva } from 'class-variance-authority' import Link from 'next/link' @@ -16,7 +17,7 @@ export default function Sidebar({ children, className }: SidebarProps) { className={cn('grid overflow-hidden lg:grid-cols-[280px_1fr]', className)} >
-
+
+ +
+ +
{children}
diff --git a/ee/tabby-ui/app/(dashboard)/page.tsx b/ee/tabby-ui/app/(dashboard)/page.tsx index 00c7ff3..fdd777a 100644 --- a/ee/tabby-ui/app/(dashboard)/page.tsx +++ b/ee/tabby-ui/app/(dashboard)/page.tsx @@ -19,17 +19,21 @@ import { WorkerKind } from '@/lib/gql/generates/graphql' import { CopyButton } from '@/components/copy-button' import { graphql } from '@/lib/gql/generates' import { useGraphQLQuery } from '@/lib/tabby/gql' +import { useSession } from '@/lib/tabby/auth' const COMMUNITY_DIALOG_SHOWN_KEY = 'community-dialog-shown' export default function Home() { + const { status } = useSession() const [open, setOpen] = useState(false) useEffect(() => { + if (status !== 'authenticated') return + if (!localStorage.getItem(COMMUNITY_DIALOG_SHOWN_KEY)) { setOpen(true) localStorage.setItem(COMMUNITY_DIALOG_SHOWN_KEY, 'true') } - }, []) + }, [status]) return (
diff --git a/ee/tabby-ui/app/auth/components/signup.tsx b/ee/tabby-ui/app/auth/components/signup.tsx deleted file mode 100644 index d8ab4e6..0000000 --- a/ee/tabby-ui/app/auth/components/signup.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client' - -import { graphql } from '@/lib/gql/generates' -import { UserAuthForm } from './user-auth-form' -import { useSearchParams } from 'next/navigation' -import { useGraphQLQuery } from '@/lib/tabby/gql' - -export const getIsAdminInitialized = graphql(/* GraphQL */ ` - query GetIsAdminInitialized { - isAdminInitialized - } -`) - -export default function Signup() { - const { data } = useGraphQLQuery(getIsAdminInitialized) - const title = data?.isAdminInitialized - ? 'Create an account' - : 'Create an admin account' - - const searchParams = useSearchParams() - const invitationCode = searchParams.get('invitationCode') || undefined - - return ( -
-
-

{title}

-

- Fill form below to create your account -

-
- -
- ) -} diff --git a/ee/tabby-ui/app/auth/layout.tsx b/ee/tabby-ui/app/auth/layout.tsx new file mode 100644 index 0000000..e8cae81 --- /dev/null +++ b/ee/tabby-ui/app/auth/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children +}: { + children: React.ReactNode +}) { + return ( +
+ {children} +
+ ) +} diff --git a/ee/tabby-ui/app/auth/page.tsx b/ee/tabby-ui/app/auth/page.tsx deleted file mode 100644 index f6a9db4..0000000 --- a/ee/tabby-ui/app/auth/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Metadata } from 'next' - -import Signup from './components/signup' - -export const metadata: Metadata = { - title: 'Authentication', - description: 'Authentication forms built using the components.' -} - -export default function AuthenticationPage() { - return ( -
- -
- ) -} diff --git a/ee/tabby-ui/app/auth/signin/components/signin.tsx b/ee/tabby-ui/app/auth/signin/components/signin.tsx new file mode 100644 index 0000000..1fc0929 --- /dev/null +++ b/ee/tabby-ui/app/auth/signin/components/signin.tsx @@ -0,0 +1,17 @@ +'use client' + +import UserSignInForm from './user-signin-form' + +export default function Signin() { + return ( +
+
+

Sign In

+

+ Enter credentials to login to your account +

+
+ +
+ ) +} diff --git a/ee/tabby-ui/app/auth/signin/components/user-signin-form.tsx b/ee/tabby-ui/app/auth/signin/components/user-signin-form.tsx new file mode 100644 index 0000000..fb85323 --- /dev/null +++ b/ee/tabby-ui/app/auth/signin/components/user-signin-form.tsx @@ -0,0 +1,113 @@ +'use client' + +import * as React from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +import { cn } from '@/lib/utils' +import { IconSpinner } from '@/components/ui/icons' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { graphql } from '@/lib/gql/generates' +import { useGraphQLForm } from '@/lib/tabby/gql' +import { useSignIn } from '@/lib/tabby/auth' +import { useRouter } from 'next/navigation' + +export const tokenAuth = graphql(/* GraphQL */ ` + mutation tokenAuth($email: String!, $password: String!) { + tokenAuth(email: $email, password: $password) { + accessToken + refreshToken + } + } +`) + +const formSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string() +}) + +interface UserAuthFormProps extends React.HTMLAttributes { + invitationCode?: string +} + +export default function UserSignInForm({ + className, + invitationCode, + ...props +}: UserAuthFormProps) { + const form = useForm>({ + resolver: zodResolver(formSchema) + }) + + const router = useRouter() + const signIn = useSignIn() + const { isSubmitting } = form.formState + const { onSubmit } = useGraphQLForm(tokenAuth, { + onSuccess: async values => { + if (await signIn(values.tokenAuth)) { + router.replace('/') + } + }, + onError: (path, message) => form.setError(path as any, { message }) + }) + + return ( +
+
+ + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + + + + +
+ ) +} diff --git a/ee/tabby-ui/app/auth/signin/page.tsx b/ee/tabby-ui/app/auth/signin/page.tsx new file mode 100644 index 0000000..07806cf --- /dev/null +++ b/ee/tabby-ui/app/auth/signin/page.tsx @@ -0,0 +1,10 @@ +import { Metadata } from 'next' +import Signin from './components/signin' + +export const metadata: Metadata = { + title: 'Sign In' +} + +export default function Page() { + return +} diff --git a/ee/tabby-ui/app/auth/signup/components/signup.tsx b/ee/tabby-ui/app/auth/signup/components/signup.tsx new file mode 100644 index 0000000..4b53c70 --- /dev/null +++ b/ee/tabby-ui/app/auth/signup/components/signup.tsx @@ -0,0 +1,50 @@ +'use client' + +import { UserAuthForm } from './user-register-form' +import { useSearchParams } from 'next/navigation' + +export default function Signup() { + const searchParams = useSearchParams() + const invitationCode = searchParams.get('invitationCode') || undefined + const isAdmin = searchParams.get('isAdmin') || false + + const title = isAdmin ? 'Create an admin account' : 'Create an account' + + const description = isAdmin + ? 'The admin account has access to invite collaborators and manage Tabby configuration' + : 'Fill form below to create your account' + + if (isAdmin || invitationCode) { + return + } else { + return ( + + ) + } +} + +function Content({ + title, + description, + show +}: { + title: string + description: string + show?: boolean +}) { + const searchParams = useSearchParams() + const invitationCode = searchParams.get('invitationCode') || undefined + + return ( +
+
+

{title}

+

{description}

+
+ {show && } +
+ ) +} diff --git a/ee/tabby-ui/app/auth/components/user-auth-form.tsx b/ee/tabby-ui/app/auth/signup/components/user-register-form.tsx similarity index 90% rename from ee/tabby-ui/app/auth/components/user-auth-form.tsx rename to ee/tabby-ui/app/auth/signup/components/user-register-form.tsx index 378add8..1eba9c5 100644 --- a/ee/tabby-ui/app/auth/components/user-auth-form.tsx +++ b/ee/tabby-ui/app/auth/signup/components/user-register-form.tsx @@ -20,6 +20,8 @@ import { } from '@/components/ui/form' import { graphql } from '@/lib/gql/generates' import { useGraphQLForm } from '@/lib/tabby/gql' +import { useSignIn } from '@/lib/tabby/auth' +import { useRouter } from 'next/navigation' export const registerUser = graphql(/* GraphQL */ ` mutation register( @@ -59,15 +61,19 @@ export function UserAuthForm({ const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - email: '', - password1: '', - password2: '', invitationCode } }) + const router = useRouter() + const signIn = useSignIn() const { isSubmitting } = form.formState const { onSubmit } = useGraphQLForm(registerUser, { + onSuccess: async values => { + if (await signIn(values.register)) { + router.replace('/') + } + }, onError: (path, message) => form.setError(path as any, { message }) }) @@ -125,14 +131,14 @@ export function UserAuthForm({ control={form.control} name="invitationCode" render={({ field }) => ( - + )} /> -