From c303c00b68e115a786be8e680528a2e24a296355 Mon Sep 17 00:00:00 2001 From: Meng Zhang Date: Fri, 8 Dec 2023 12:16:26 +0800 Subject: [PATCH] feat(webserver): implement optional secure access for dashboard (#983) * feat: implement optional access secure for login flow * update * fix * remove useless import * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../app/auth/signup/components/signup.tsx | 2 +- ee/tabby-ui/components/header.tsx | 33 ----------- ee/tabby-ui/components/ui/icons.tsx | 24 +++++++- ee/tabby-ui/components/user-panel.tsx | 47 +++++++++++++--- ee/tabby-ui/lib/gql/generates/gql.ts | 18 +++--- ee/tabby-ui/lib/gql/generates/graphql.ts | 56 +++++++++---------- ee/tabby-ui/lib/tabby/auth.tsx | 39 ++++++++++++- 7 files changed, 138 insertions(+), 81 deletions(-) diff --git a/ee/tabby-ui/app/auth/signup/components/signup.tsx b/ee/tabby-ui/app/auth/signup/components/signup.tsx index 4b53c70..c756eaf 100644 --- a/ee/tabby-ui/app/auth/signup/components/signup.tsx +++ b/ee/tabby-ui/app/auth/signup/components/signup.tsx @@ -11,7 +11,7 @@ export default function Signup() { 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' + ? 'After creating an admin account, your instance is secured, and only registered users can access it.' : 'Fill form below to create your account' if (isAdmin || invitationCode) { diff --git a/ee/tabby-ui/components/header.tsx b/ee/tabby-ui/components/header.tsx index e0a682a..28f59f7 100644 --- a/ee/tabby-ui/components/header.tsx +++ b/ee/tabby-ui/components/header.tsx @@ -12,14 +12,8 @@ import { useWorkers } from '@/lib/hooks/use-workers' import { WorkerKind } from '@/lib/gql/generates/graphql' import { has } from 'lodash-es' 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() { - useRequireAuth() - const { data } = useHealth() const workers = useWorkers(data) const isChatEnabled = has(workers, WorkerKind.Chat) @@ -80,30 +74,3 @@ function isNewVersionAvailable(version?: string, latestRelease?: ReleaseInfo) { 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]) -} diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index dd4de39..eaad117 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -594,6 +594,27 @@ function IconLogout({ className, ...props }: React.ComponentProps<'svg'>) { ) } +function IconUnlock({ className, ...props }: React.ComponentProps<'svg'>) { + return ( + + + + + ) +} + export { IconEdit, IconNextChat, @@ -625,5 +646,6 @@ export { IconSlack, IconNotice, IconSymbolFunction, - IconLogout + IconLogout, + IconUnlock } diff --git a/ee/tabby-ui/components/user-panel.tsx b/ee/tabby-ui/components/user-panel.tsx index a5953bb..9a7b111 100644 --- a/ee/tabby-ui/components/user-panel.tsx +++ b/ee/tabby-ui/components/user-panel.tsx @@ -1,20 +1,53 @@ -import { useSession, useSignOut } from '@/lib/tabby/auth' +import { + useAuthenticatedSession, + useIsAdminInitialized, + useSession, + useSignOut +} from '@/lib/tabby/auth' import { cn } from '@/lib/utils' -import { IconLogout } from './ui/icons' +import { IconLogout, IconUnlock } from './ui/icons' +import Link from 'next/link' +import React from 'react' export default function UserPanel() { - const { data: session, status } = useSession() - const signOut = useSignOut() + const isAdminInitialized = useIsAdminInitialized() + + const Component = isAdminInitialized ? UserInfoPanel : EnableAdminPanel - if (status !== 'authenticated') return return (
- + +
+ ) +} + +function UserInfoPanel({ className }: React.ComponentProps<'span'>) { + const session = useAuthenticatedSession() + const signOut = useSignOut() + + return ( + session && ( + {session.email} - + ) + ) +} + +function EnableAdminPanel({ className }: React.ComponentProps<'span'>) { + return ( + + Secure Access + ) } diff --git a/ee/tabby-ui/lib/gql/generates/gql.ts b/ee/tabby-ui/lib/gql/generates/gql.ts index 8b0d1b5..5a44e75 100644 --- a/ee/tabby-ui/lib/gql/generates/gql.ts +++ b/ee/tabby-ui/lib/gql/generates/gql.ts @@ -19,12 +19,12 @@ const documents = { 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': 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': types.GetWorkersDocument, '\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\n accessToken\n refreshToken\n }\n }\n': - types.RefreshTokenDocument + types.RefreshTokenDocument, + '\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n': + types.GetIsAdminInitializedDocument } /** @@ -59,12 +59,6 @@ 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' ): (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. */ @@ -77,6 +71,12 @@ export function graphql( export function graphql( source: '\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\n accessToken\n refreshToken\n }\n }\n' ): (typeof documents)['\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\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'] export function graphql(source: string) { return (documents as any)[source] ?? {} diff --git a/ee/tabby-ui/lib/gql/generates/graphql.ts b/ee/tabby-ui/lib/gql/generates/graphql.ts index 1d821bd..6b9d6c2 100644 --- a/ee/tabby-ui/lib/gql/generates/graphql.ts +++ b/ee/tabby-ui/lib/gql/generates/graphql.ts @@ -176,15 +176,6 @@ 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 GetWorkersQuery = { @@ -215,6 +206,15 @@ export type RefreshTokenMutation = { } } +export type GetIsAdminInitializedQueryVariables = Exact<{ + [key: string]: never +}> + +export type GetIsAdminInitializedQuery = { + __typename?: 'Query' + isAdminInitialized: boolean +} + export const GetRegistrationTokenDocument = { kind: 'Document', definitions: [ @@ -405,25 +405,6 @@ export const RegisterDocument = { } ] } as unknown as DocumentNode -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 = { kind: 'Document', definitions: [ @@ -508,3 +489,22 @@ export const RefreshTokenDocument = { RefreshTokenMutation, RefreshTokenMutationVariables > +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 +> diff --git a/ee/tabby-ui/lib/tabby/auth.tsx b/ee/tabby-ui/lib/tabby/auth.tsx index 5d55c67..b627c5a 100644 --- a/ee/tabby-ui/lib/tabby/auth.tsx +++ b/ee/tabby-ui/lib/tabby/auth.tsx @@ -1,8 +1,9 @@ import * as React from 'react' import { graphql } from '@/lib/gql/generates' import useInterval from '@/lib/hooks/use-interval' -import { gqlClient } from '@/lib/tabby/gql' +import { gqlClient, useGraphQLQuery } from '@/lib/tabby/gql' import { jwtDecode } from 'jwt-decode' +import { useRouter } from 'next/navigation' interface AuthData { accessToken: string @@ -241,6 +242,40 @@ function useSession(): Session { } } +export const getIsAdminInitialized = graphql(/* GraphQL */ ` + query GetIsAdminInitialized { + isAdminInitialized + } +`) + +function useIsAdminInitialized() { + const { data } = useGraphQLQuery(getIsAdminInitialized) + return data?.isAdminInitialized +} + +function useAuthenticatedSession() { + const { data } = useGraphQLQuery(getIsAdminInitialized) + const router = useRouter() + const { data: session, status } = useSession() + + React.useEffect(() => { + if (!data?.isAdminInitialized) return + + if (status === 'unauthenticated') { + router.replace('/auth/signin') + } + }, [data, status]) + + return session +} + export type { AuthStore, User, Session } -export { AuthProvider, useSignIn, useSignOut, useSession } +export { + AuthProvider, + useSignIn, + useSignOut, + useSession, + useIsAdminInitialized, + useAuthenticatedSession +}