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>support-auth-token
parent
2df3b1c00d
commit
779eef0c1f
|
|
@ -1,5 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import UserPanel from '@/components/user-panel'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { cva } from 'class-variance-authority'
|
import { cva } from 'class-variance-authority'
|
||||||
import Link from 'next/link'
|
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)}
|
className={cn('grid overflow-hidden lg:grid-cols-[280px_1fr]', className)}
|
||||||
>
|
>
|
||||||
<div className="hidden border-r bg-zinc-100/40 dark:bg-zinc-800/40 lg:block">
|
<div className="hidden border-r bg-zinc-100/40 dark:bg-zinc-800/40 lg:block">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2 h-full">
|
||||||
<div className="h-[12px]"></div>
|
<div className="h-[12px]"></div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<nav className="grid items-start gap-4 px-4 text-sm font-medium">
|
<nav className="grid items-start gap-4 px-4 text-sm font-medium">
|
||||||
|
|
@ -62,6 +63,10 @@ export default function Sidebar({ children, className }: SidebarProps) {
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto">
|
||||||
|
<UserPanel />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col overflow-auto">{children}</div>
|
<div className="flex flex-1 flex-col overflow-auto">{children}</div>
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,21 @@ import { WorkerKind } from '@/lib/gql/generates/graphql'
|
||||||
import { CopyButton } from '@/components/copy-button'
|
import { CopyButton } from '@/components/copy-button'
|
||||||
import { graphql } from '@/lib/gql/generates'
|
import { graphql } from '@/lib/gql/generates'
|
||||||
import { useGraphQLQuery } from '@/lib/tabby/gql'
|
import { useGraphQLQuery } from '@/lib/tabby/gql'
|
||||||
|
import { useSession } from '@/lib/tabby/auth'
|
||||||
|
|
||||||
const COMMUNITY_DIALOG_SHOWN_KEY = 'community-dialog-shown'
|
const COMMUNITY_DIALOG_SHOWN_KEY = 'community-dialog-shown'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { status } = useSession()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (status !== 'authenticated') return
|
||||||
|
|
||||||
if (!localStorage.getItem(COMMUNITY_DIALOG_SHOWN_KEY)) {
|
if (!localStorage.getItem(COMMUNITY_DIALOG_SHOWN_KEY)) {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
localStorage.setItem(COMMUNITY_DIALOG_SHOWN_KEY, 'true')
|
localStorage.setItem(COMMUNITY_DIALOG_SHOWN_KEY, 'true')
|
||||||
}
|
}
|
||||||
}, [])
|
}, [status])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 lg:p-16">
|
<div className="p-4 lg:p-16">
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="space-y-6 w-[350px]">
|
|
||||||
<div className="flex flex-col space-y-2 text-center">
|
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Fill form below to create your account
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<UserAuthForm invitationCode={invitationCode} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export default function RootLayout({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center flex-1">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="flex flex-col items-center justify-center flex-1">
|
|
||||||
<Signup />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import UserSignInForm from './user-signin-form'
|
||||||
|
|
||||||
|
export default function Signin() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 w-[350px]">
|
||||||
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Sign In</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Enter credentials to login to your account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<UserSignInForm />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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<HTMLDivElement> {
|
||||||
|
invitationCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserSignInForm({
|
||||||
|
className,
|
||||||
|
invitationCode,
|
||||||
|
...props
|
||||||
|
}: UserAuthFormProps) {
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
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 (
|
||||||
|
<div className={cn('grid gap-6', className)} {...props}>
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="grid gap-2" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
type="email"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="email"
|
||||||
|
autoCorrect="off"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="mt-1" disabled={isSubmitting}>
|
||||||
|
{isSubmitting && (
|
||||||
|
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
<FormMessage className="text-center" />
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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 <Signin />
|
||||||
|
}
|
||||||
|
|
@ -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 <Content title={title} description={description} show />
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Content
|
||||||
|
title="No invitation code"
|
||||||
|
description="Please contact your Tabby admin for an invitation code to register"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
show
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
show?: boolean
|
||||||
|
}) {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const invitationCode = searchParams.get('invitationCode') || undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 w-[350px]">
|
||||||
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
</div>
|
||||||
|
{show && <UserAuthForm invitationCode={invitationCode} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,8 @@ import {
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { graphql } from '@/lib/gql/generates'
|
import { graphql } from '@/lib/gql/generates'
|
||||||
import { useGraphQLForm } from '@/lib/tabby/gql'
|
import { useGraphQLForm } from '@/lib/tabby/gql'
|
||||||
|
import { useSignIn } from '@/lib/tabby/auth'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
export const registerUser = graphql(/* GraphQL */ `
|
export const registerUser = graphql(/* GraphQL */ `
|
||||||
mutation register(
|
mutation register(
|
||||||
|
|
@ -59,15 +61,19 @@ export function UserAuthForm({
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
|
||||||
password1: '',
|
|
||||||
password2: '',
|
|
||||||
invitationCode
|
invitationCode
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const signIn = useSignIn()
|
||||||
const { isSubmitting } = form.formState
|
const { isSubmitting } = form.formState
|
||||||
const { onSubmit } = useGraphQLForm(registerUser, {
|
const { onSubmit } = useGraphQLForm(registerUser, {
|
||||||
|
onSuccess: async values => {
|
||||||
|
if (await signIn(values.register)) {
|
||||||
|
router.replace('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
onError: (path, message) => form.setError(path as any, { message })
|
onError: (path, message) => form.setError(path as any, { message })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -125,14 +131,14 @@ export function UserAuthForm({
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="invitationCode"
|
name="invitationCode"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="hidden">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="hidden" {...field} />
|
<Input type="hidden" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<Button type="submit" className="mt-1" disabled={isSubmitting}>
|
||||||
{isSubmitting && (
|
{isSubmitting && (
|
||||||
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
|
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Metadata } from 'next'
|
||||||
|
import Signup from './components/signup'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Sign Up'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <Signup />
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import { Button } from '@/components/ui/button'
|
||||||
import { ListSkeleton } from '@/components/skeleton'
|
import { ListSkeleton } from '@/components/skeleton'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { ClearChatsButton } from './clear-chats-button'
|
import { ClearChatsButton } from './clear-chats-button'
|
||||||
|
import UserPanel from '@/components/user-panel'
|
||||||
|
|
||||||
interface ChatSessionsProps {
|
interface ChatSessionsProps {
|
||||||
className?: string
|
className?: string
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,14 @@ import { useWorkers } from '@/lib/hooks/use-workers'
|
||||||
import { WorkerKind } from '@/lib/gql/generates/graphql'
|
import { WorkerKind } from '@/lib/gql/generates/graphql'
|
||||||
import { has } from 'lodash-es'
|
import { has } from 'lodash-es'
|
||||||
import { ThemeToggle } from './theme-toggle'
|
import { ThemeToggle } from './theme-toggle'
|
||||||
|
import { useSession } from '@/lib/tabby/auth'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { graphql } from '@/lib/gql/generates'
|
||||||
|
import { useGraphQLQuery } from '@/lib/tabby/gql'
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
|
useRequireAuth()
|
||||||
|
|
||||||
const { data } = useHealth()
|
const { data } = useHealth()
|
||||||
const workers = useWorkers(data)
|
const workers = useWorkers(data)
|
||||||
const isChatEnabled = has(workers, WorkerKind.Chat)
|
const isChatEnabled = has(workers, WorkerKind.Chat)
|
||||||
|
|
@ -74,3 +80,30 @@ function isNewVersionAvailable(version?: string, latestRelease?: ReleaseInfo) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getIsAdminInitialized = graphql(/* GraphQL */ `
|
||||||
|
query GetIsAdminInitialized {
|
||||||
|
isAdminInitialized
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
function useRequireAuth() {
|
||||||
|
const { data, isLoading } = useGraphQLQuery(
|
||||||
|
getIsAdminInitialized,
|
||||||
|
undefined,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
const router = useRouter()
|
||||||
|
const { status } = useSession()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isLoading) return
|
||||||
|
if (status !== 'unauthenticated') return
|
||||||
|
|
||||||
|
if (data!.isAdminInitialized) {
|
||||||
|
router.replace('/auth/signin')
|
||||||
|
} else {
|
||||||
|
router.replace('/auth/signup?isAdmin=true')
|
||||||
|
}
|
||||||
|
}, [data, isLoading, status])
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -572,6 +572,28 @@ function IconSymbolFunction({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IconLogout({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={cn('h-4 w-4', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||||
|
<polyline points="16 17 21 12 16 7" />
|
||||||
|
<line x1="21" x2="9" y1="12" y2="12" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconNextChat,
|
IconNextChat,
|
||||||
|
|
@ -602,5 +624,6 @@ export {
|
||||||
IconChevronUpDown,
|
IconChevronUpDown,
|
||||||
IconSlack,
|
IconSlack,
|
||||||
IconNotice,
|
IconNotice,
|
||||||
IconSymbolFunction
|
IconSymbolFunction,
|
||||||
|
IconLogout
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useSession, useSignOut } from '@/lib/tabby/auth'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { IconLogout } from './ui/icons'
|
||||||
|
|
||||||
|
export default function UserPanel() {
|
||||||
|
const { data: session, status } = useSession()
|
||||||
|
const signOut = useSignOut()
|
||||||
|
|
||||||
|
if (status !== 'authenticated') return
|
||||||
|
return (
|
||||||
|
<div className="py-4 flex justify-center text-sm font-medium">
|
||||||
|
<span className={cn('flex items-center gap-2')}>
|
||||||
|
<span title="Sign out">
|
||||||
|
<IconLogout className="cursor-pointer" onClick={signOut} />
|
||||||
|
</span>
|
||||||
|
{session.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,12 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
|
||||||
const documents = {
|
const documents = {
|
||||||
'\n query GetRegistrationToken {\n registrationToken\n }\n':
|
'\n query GetRegistrationToken {\n registrationToken\n }\n':
|
||||||
types.GetRegistrationTokenDocument,
|
types.GetRegistrationTokenDocument,
|
||||||
'\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n':
|
'\n mutation tokenAuth($email: String!, $password: String!) {\n tokenAuth(email: $email, password: $password) {\n accessToken\n refreshToken\n }\n }\n':
|
||||||
types.GetIsAdminInitializedDocument,
|
types.TokenAuthDocument,
|
||||||
'\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n':
|
'\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n':
|
||||||
types.RegisterDocument,
|
types.RegisterDocument,
|
||||||
|
'\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n':
|
||||||
|
types.GetIsAdminInitializedDocument,
|
||||||
'\n query GetWorkers {\n workers {\n kind\n name\n addr\n device\n arch\n cpuInfo\n cpuCount\n cudaDevices\n }\n }\n':
|
'\n query GetWorkers {\n workers {\n kind\n name\n addr\n device\n arch\n cpuInfo\n cpuCount\n cudaDevices\n }\n }\n':
|
||||||
types.GetWorkersDocument,
|
types.GetWorkersDocument,
|
||||||
'\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\n accessToken\n refreshToken\n }\n }\n':
|
'\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\n accessToken\n refreshToken\n }\n }\n':
|
||||||
|
|
@ -49,14 +51,20 @@ export function graphql(
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: '\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n'
|
source: '\n mutation tokenAuth($email: String!, $password: String!) {\n tokenAuth(email: $email, password: $password) {\n accessToken\n refreshToken\n }\n }\n'
|
||||||
): (typeof documents)['\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n']
|
): (typeof documents)['\n mutation tokenAuth($email: String!, $password: String!) {\n tokenAuth(email: $email, password: $password) {\n accessToken\n refreshToken\n }\n }\n']
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: '\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n'
|
source: '\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n'
|
||||||
): (typeof documents)['\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n']
|
): (typeof documents)['\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n']
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(
|
||||||
|
source: '\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n'
|
||||||
|
): (typeof documents)['\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n']
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -146,13 +146,18 @@ export type GetRegistrationTokenQuery = {
|
||||||
registrationToken: string
|
registrationToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetIsAdminInitializedQueryVariables = Exact<{
|
export type TokenAuthMutationVariables = Exact<{
|
||||||
[key: string]: never
|
email: Scalars['String']['input']
|
||||||
|
password: Scalars['String']['input']
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type GetIsAdminInitializedQuery = {
|
export type TokenAuthMutation = {
|
||||||
__typename?: 'Query'
|
__typename?: 'Mutation'
|
||||||
isAdminInitialized: boolean
|
tokenAuth: {
|
||||||
|
__typename?: 'TokenAuthResponse'
|
||||||
|
accessToken: string
|
||||||
|
refreshToken: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RegisterMutationVariables = Exact<{
|
export type RegisterMutationVariables = Exact<{
|
||||||
|
|
@ -171,6 +176,15 @@ export type RegisterMutation = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GetIsAdminInitializedQueryVariables = Exact<{
|
||||||
|
[key: string]: never
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type GetIsAdminInitializedQuery = {
|
||||||
|
__typename?: 'Query'
|
||||||
|
isAdminInitialized: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type GetWorkersQueryVariables = Exact<{ [key: string]: never }>
|
export type GetWorkersQueryVariables = Exact<{ [key: string]: never }>
|
||||||
|
|
||||||
export type GetWorkersQuery = {
|
export type GetWorkersQuery = {
|
||||||
|
|
@ -220,25 +234,74 @@ export const GetRegistrationTokenDocument = {
|
||||||
GetRegistrationTokenQuery,
|
GetRegistrationTokenQuery,
|
||||||
GetRegistrationTokenQueryVariables
|
GetRegistrationTokenQueryVariables
|
||||||
>
|
>
|
||||||
export const GetIsAdminInitializedDocument = {
|
export const TokenAuthDocument = {
|
||||||
kind: 'Document',
|
kind: 'Document',
|
||||||
definitions: [
|
definitions: [
|
||||||
{
|
{
|
||||||
kind: 'OperationDefinition',
|
kind: 'OperationDefinition',
|
||||||
operation: 'query',
|
operation: 'mutation',
|
||||||
name: { kind: 'Name', value: 'GetIsAdminInitialized' },
|
name: { kind: 'Name', value: 'tokenAuth' },
|
||||||
|
variableDefinitions: [
|
||||||
|
{
|
||||||
|
kind: 'VariableDefinition',
|
||||||
|
variable: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'email' }
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
kind: 'NonNullType',
|
||||||
|
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'VariableDefinition',
|
||||||
|
variable: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'password' }
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
kind: 'NonNullType',
|
||||||
|
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: 'SelectionSet',
|
kind: 'SelectionSet',
|
||||||
selections: [
|
selections: [
|
||||||
{ kind: 'Field', name: { kind: 'Name', value: 'isAdminInitialized' } }
|
{
|
||||||
|
kind: 'Field',
|
||||||
|
name: { kind: 'Name', value: 'tokenAuth' },
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
kind: 'Argument',
|
||||||
|
name: { kind: 'Name', value: 'email' },
|
||||||
|
value: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'email' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'Argument',
|
||||||
|
name: { kind: 'Name', value: 'password' },
|
||||||
|
value: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'password' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selectionSet: {
|
||||||
|
kind: 'SelectionSet',
|
||||||
|
selections: [
|
||||||
|
{ kind: 'Field', name: { kind: 'Name', value: 'accessToken' } },
|
||||||
|
{ kind: 'Field', name: { kind: 'Name', value: 'refreshToken' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} as unknown as DocumentNode<
|
} as unknown as DocumentNode<TokenAuthMutation, TokenAuthMutationVariables>
|
||||||
GetIsAdminInitializedQuery,
|
|
||||||
GetIsAdminInitializedQueryVariables
|
|
||||||
>
|
|
||||||
export const RegisterDocument = {
|
export const RegisterDocument = {
|
||||||
kind: 'Document',
|
kind: 'Document',
|
||||||
definitions: [
|
definitions: [
|
||||||
|
|
@ -342,6 +405,25 @@ export const RegisterDocument = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} as unknown as DocumentNode<RegisterMutation, RegisterMutationVariables>
|
} as unknown as DocumentNode<RegisterMutation, RegisterMutationVariables>
|
||||||
|
export const GetIsAdminInitializedDocument = {
|
||||||
|
kind: 'Document',
|
||||||
|
definitions: [
|
||||||
|
{
|
||||||
|
kind: 'OperationDefinition',
|
||||||
|
operation: 'query',
|
||||||
|
name: { kind: 'Name', value: 'GetIsAdminInitialized' },
|
||||||
|
selectionSet: {
|
||||||
|
kind: 'SelectionSet',
|
||||||
|
selections: [
|
||||||
|
{ kind: 'Field', name: { kind: 'Name', value: 'isAdminInitialized' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as unknown as DocumentNode<
|
||||||
|
GetIsAdminInitializedQuery,
|
||||||
|
GetIsAdminInitializedQueryVariables
|
||||||
|
>
|
||||||
export const GetWorkersDocument = {
|
export const GetWorkersDocument = {
|
||||||
kind: 'Document',
|
kind: 'Document',
|
||||||
definitions: [
|
definitions: [
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,11 @@ const AuthProvider: React.FunctionComponent<AuthProviderProps> = ({
|
||||||
data: null
|
data: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const initialized = React.useRef(false)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (initialized.current) return
|
||||||
|
|
||||||
|
initialized.current = true
|
||||||
const data = storage.initialState()
|
const data = storage.initialState()
|
||||||
if (data?.refreshToken) {
|
if (data?.refreshToken) {
|
||||||
doRefresh(data.refreshToken, dispatch)
|
doRefresh(data.refreshToken, dispatch)
|
||||||
|
|
@ -219,9 +223,14 @@ type Session =
|
||||||
function useSession(): Session {
|
function useSession(): Session {
|
||||||
const { authState } = useAuthStore()
|
const { authState } = useAuthStore()
|
||||||
if (authState?.status == 'authenticated') {
|
if (authState?.status == 'authenticated') {
|
||||||
const { user } = jwtDecode<{ user: User }>(authState.data.accessToken)
|
const { user } = jwtDecode<{ user: { email: string; is_admin: boolean } }>(
|
||||||
|
authState.data.accessToken
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
data: user,
|
data: {
|
||||||
|
email: user.email,
|
||||||
|
isAdmin: user.is_admin
|
||||||
|
},
|
||||||
status: authState.status
|
status: authState.status
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,14 @@ export function useGraphQLForm<
|
||||||
>(
|
>(
|
||||||
document: TypedDocumentNode<TResult, TVariables>,
|
document: TypedDocumentNode<TResult, TVariables>,
|
||||||
options?: {
|
options?: {
|
||||||
|
onSuccess?: (values: TResult) => void
|
||||||
onError?: (path: string, message: string) => void
|
onError?: (path: string, message: string) => void
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const onSubmit = async (values: TVariables) => {
|
const onSubmit = async (values: TVariables) => {
|
||||||
|
let res
|
||||||
try {
|
try {
|
||||||
await gqlClient.request(document, values)
|
res = await gqlClient.request(document, values)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const { errors = [] } = (err as any).response as GraphQLResponse
|
const { errors = [] } = (err as any).response as GraphQLResponse
|
||||||
for (const error of errors) {
|
for (const error of errors) {
|
||||||
|
|
@ -42,7 +44,11 @@ export function useGraphQLForm<
|
||||||
options?.onError && options?.onError('root', error.message)
|
options?.onError && options?.onError('root', error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options?.onSuccess && options.onSuccess(res)
|
||||||
}
|
}
|
||||||
return { onSubmit }
|
return { onSubmit }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue