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>support-auth-token
parent
8b97ec5d0c
commit
c303c00b68
|
|
@ -11,7 +11,7 @@ export default function Signup() {
|
||||||
const title = isAdmin ? 'Create an admin account' : 'Create an account'
|
const title = isAdmin ? 'Create an admin account' : 'Create an account'
|
||||||
|
|
||||||
const description = isAdmin
|
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'
|
: 'Fill form below to create your account'
|
||||||
|
|
||||||
if (isAdmin || invitationCode) {
|
if (isAdmin || invitationCode) {
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,8 @@ 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)
|
||||||
|
|
@ -80,30 +74,3 @@ 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])
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -594,6 +594,27 @@ function IconLogout({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IconUnlock({ 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}
|
||||||
|
>
|
||||||
|
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 9.9-1" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconNextChat,
|
IconNextChat,
|
||||||
|
|
@ -625,5 +646,6 @@ export {
|
||||||
IconSlack,
|
IconSlack,
|
||||||
IconNotice,
|
IconNotice,
|
||||||
IconSymbolFunction,
|
IconSymbolFunction,
|
||||||
IconLogout
|
IconLogout,
|
||||||
|
IconUnlock
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { 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() {
|
export default function UserPanel() {
|
||||||
const { data: session, status } = useSession()
|
const isAdminInitialized = useIsAdminInitialized()
|
||||||
const signOut = useSignOut()
|
|
||||||
|
const Component = isAdminInitialized ? UserInfoPanel : EnableAdminPanel
|
||||||
|
|
||||||
if (status !== 'authenticated') return
|
|
||||||
return (
|
return (
|
||||||
<div className="py-4 flex justify-center text-sm font-medium">
|
<div className="py-4 flex justify-center text-sm font-medium">
|
||||||
<span className={cn('flex items-center gap-2')}>
|
<Component className={cn('flex items-center gap-2')} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserInfoPanel({ className }: React.ComponentProps<'span'>) {
|
||||||
|
const session = useAuthenticatedSession()
|
||||||
|
const signOut = useSignOut()
|
||||||
|
|
||||||
|
return (
|
||||||
|
session && (
|
||||||
|
<span className={className}>
|
||||||
<span title="Sign out">
|
<span title="Sign out">
|
||||||
<IconLogout className="cursor-pointer" onClick={signOut} />
|
<IconLogout className="cursor-pointer" onClick={signOut} />
|
||||||
</span>
|
</span>
|
||||||
{session.email}
|
{session.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnableAdminPanel({ className }: React.ComponentProps<'span'>) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={cn('cursor-pointer', className)}
|
||||||
|
title="Authentication is currently not enabled. Click to view details"
|
||||||
|
href={{
|
||||||
|
pathname: '/auth/signup',
|
||||||
|
query: { isAdmin: true }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconUnlock /> Secure Access
|
||||||
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,12 @@ const documents = {
|
||||||
types.TokenAuthDocument,
|
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':
|
||||||
types.RefreshTokenDocument
|
types.RefreshTokenDocument,
|
||||||
|
'\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n':
|
||||||
|
types.GetIsAdminInitializedDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -59,12 +59,6 @@ export function graphql(
|
||||||
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.
|
||||||
*/
|
*/
|
||||||
|
|
@ -77,6 +71,12 @@ export function graphql(
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: '\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\n accessToken\n refreshToken\n }\n }\n'
|
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']
|
): (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) {
|
export function graphql(source: string) {
|
||||||
return (documents as any)[source] ?? {}
|
return (documents as any)[source] ?? {}
|
||||||
|
|
|
||||||
|
|
@ -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 GetWorkersQueryVariables = Exact<{ [key: string]: never }>
|
||||||
|
|
||||||
export type GetWorkersQuery = {
|
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 = {
|
export const GetRegistrationTokenDocument = {
|
||||||
kind: 'Document',
|
kind: 'Document',
|
||||||
definitions: [
|
definitions: [
|
||||||
|
|
@ -405,25 +405,6 @@ 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: [
|
||||||
|
|
@ -508,3 +489,22 @@ export const RefreshTokenDocument = {
|
||||||
RefreshTokenMutation,
|
RefreshTokenMutation,
|
||||||
RefreshTokenMutationVariables
|
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
|
||||||
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { graphql } from '@/lib/gql/generates'
|
import { graphql } from '@/lib/gql/generates'
|
||||||
import useInterval from '@/lib/hooks/use-interval'
|
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 { jwtDecode } from 'jwt-decode'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
interface AuthData {
|
interface AuthData {
|
||||||
accessToken: string
|
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 type { AuthStore, User, Session }
|
||||||
|
|
||||||
export { AuthProvider, useSignIn, useSignOut, useSession }
|
export {
|
||||||
|
AuthProvider,
|
||||||
|
useSignIn,
|
||||||
|
useSignOut,
|
||||||
|
useSession,
|
||||||
|
useIsAdminInitialized,
|
||||||
|
useAuthenticatedSession
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue