diff --git a/packages/frontend/admin/src/app.tsx b/packages/frontend/admin/src/app.tsx index f8e6be011b..38cb50a48a 100644 --- a/packages/frontend/admin/src/app.tsx +++ b/packages/frontend/admin/src/app.tsx @@ -4,12 +4,16 @@ import { wrapCreateBrowserRouter } from '@sentry/react'; import { useEffect } from 'react'; import { createBrowserRouter as reactRouterCreateBrowserRouter, + Navigate, + Outlet, RouterProvider, useLocation, - useNavigate, } from 'react-router-dom'; +import { toast } from 'sonner'; import { TooltipProvider } from './components/ui/tooltip'; +import { isAdmin, useCurrentUser, useServerConfig } from './modules/common'; +import { Layout } from './modules/layout'; const createBrowserRouter = wrapCreateBrowserRouter( reactRouterCreateBrowserRouter @@ -19,53 +23,76 @@ const _createBrowserRouter = window.SENTRY_RELEASE ? createBrowserRouter : reactRouterCreateBrowserRouter; -const Redirect = function Redirect() { - const location = useLocation(); - const navigate = useNavigate(); +function AuthenticatedRoutes() { + const user = useCurrentUser(); + useEffect(() => { - if (!location.pathname.startsWith('/admin/accounts')) { - navigate('/admin/accounts', { replace: true }); + if (user && !isAdmin(user)) { + toast.error('You are not an admin, please login the admin account.'); } - }, [location, navigate]); - return null; -}; + }, [user]); + + if (!user || !isAdmin(user)) { + return ; + } + + return ( + + + + ); +} + +function RootRoutes() { + const config = useServerConfig(); + const location = useLocation(); + + if (!config.initialized) { + return ; + } + + if (/^\/admin\/?$/.test(location.pathname)) { + return ; + } + + return ; +} export const router = _createBrowserRouter( [ - { - path: '/', - element: , - }, { path: '/admin', + element: , children: [ - { - path: '', - element: , - }, - { - path: '/admin/accounts', - lazy: () => import('./modules/accounts'), - }, { path: '/admin/auth', lazy: () => import('./modules/auth'), }, - { - path: '/admin/ai', - lazy: () => import('./modules/ai'), - }, { path: '/admin/setup', lazy: () => import('./modules/setup'), }, { - path: '/admin/config', - lazy: () => import('./modules/config'), - }, - { - path: '/admin/settings', - lazy: () => import('./modules/settings'), + path: '/admin/*', + element: , + children: [ + { + path: 'accounts', + lazy: () => import('./modules/accounts'), + }, + // { + // path: 'ai', + // lazy: () => import('./modules/ai'), + // }, + { + path: 'config', + lazy: () => import('./modules/config'), + }, + { + path: 'settings', + lazy: () => import('./modules/settings'), + }, + ], }, ], }, diff --git a/packages/frontend/admin/src/modules/accounts/index.tsx b/packages/frontend/admin/src/modules/accounts/index.tsx index 27b5955e7b..eafa0b2a72 100644 --- a/packages/frontend/admin/src/modules/accounts/index.tsx +++ b/packages/frontend/admin/src/modules/accounts/index.tsx @@ -3,14 +3,9 @@ import { useQuery } from '@affine/core/hooks/use-query'; import { listUsersQuery } from '@affine/graphql'; import { useState } from 'react'; -import { Layout } from '../layout'; import { columns } from './components/columns'; import { DataTable } from './components/data-table'; -export function Accounts() { - return } />; -} - export function AccountPage() { const [pagination, setPagination] = useState({ pageIndex: 0, @@ -45,4 +40,4 @@ export function AccountPage() { ); } -export { Accounts as Component }; +export { AccountPage as Component }; diff --git a/packages/frontend/admin/src/modules/ai/index.tsx b/packages/frontend/admin/src/modules/ai/index.tsx index cc4eb21e04..2cbdfdb59b 100644 --- a/packages/frontend/admin/src/modules/ai/index.tsx +++ b/packages/frontend/admin/src/modules/ai/index.tsx @@ -8,7 +8,7 @@ export function Ai() { return null; // hide ai config in admin until it's ready - // return } />; + // return ; } export function AiPage() { diff --git a/packages/frontend/admin/src/modules/auth/index.tsx b/packages/frontend/admin/src/modules/auth/index.tsx index c350d29b31..6ec83baa21 100644 --- a/packages/frontend/admin/src/modules/auth/index.tsx +++ b/packages/frontend/admin/src/modules/auth/index.tsx @@ -1,90 +1,80 @@ import { Button } from '@affine/admin/components/ui/button'; import { Input } from '@affine/admin/components/ui/input'; import { Label } from '@affine/admin/components/ui/label'; -import { useMutateQueryResource } from '@affine/core/hooks/use-mutation'; -import { - FeatureType, - getCurrentUserFeaturesQuery, - getUserFeaturesQuery, -} from '@affine/graphql'; -import { useCallback, useEffect, useRef } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { FeatureType, getUserFeaturesQuery } from '@affine/graphql'; +import type { FormEvent } from 'react'; +import { useCallback, useRef } from 'react'; +import { Navigate } from 'react-router-dom'; import { toast } from 'sonner'; -import { useCurrentUser, useServerConfig } from '../common'; +import { isAdmin, useCurrentUser, useRevalidateCurrentUser } from '../common'; import logo from './logo.svg'; export function Auth() { const currentUser = useCurrentUser(); - const serverConfig = useServerConfig(); - const revalidate = useMutateQueryResource(); + const revalidate = useRevalidateCurrentUser(); const emailRef = useRef(null); const passwordRef = useRef(null); - const navigate = useNavigate(); - const login = useCallback(() => { - if (!emailRef.current || !passwordRef.current) return; - fetch('/api/auth/sign-in', { - method: 'POST', - body: JSON.stringify({ - email: emailRef.current?.value, - password: passwordRef.current?.value, - }), - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(async response => { - if (!response.ok) { - const data = await response.json(); - throw new Error(data.message || 'Failed to login'); - } - await revalidate(getCurrentUserFeaturesQuery); - return response.json(); + const login = useCallback( + (e: FormEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (!emailRef.current || !passwordRef.current) return; + fetch('/api/auth/sign-in', { + method: 'POST', + body: JSON.stringify({ + email: emailRef.current?.value, + password: passwordRef.current?.value, + }), + headers: { + 'Content-Type': 'application/json', + }, }) - .then(() => - fetch('/graphql', { - method: 'POST', - body: JSON.stringify({ - operationName: getUserFeaturesQuery.operationName, - query: getUserFeaturesQuery.query, - variables: {}, - }), - headers: { - 'Content-Type': 'application/json', - }, - }) - ) - .then(res => res.json()) - .then( - ({ - data: { - currentUser: { features }, - }, - }) => { - if (features.includes(FeatureType.Admin)) { - toast.success('Logged in successfully'); - navigate('/admin'); - } else { - toast.error('You are not an admin'); + .then(async response => { + if (!response.ok) { + const data = await response.json(); + throw new Error(data.message || 'Failed to login'); } - } - ) - .catch(err => { - toast.error(`Failed to login: ${err.message}`); - }); - }, [navigate, revalidate]); + return response.json(); + }) + .then(() => + fetch('/graphql', { + method: 'POST', + body: JSON.stringify({ + operationName: getUserFeaturesQuery.operationName, + query: getUserFeaturesQuery.query, + variables: {}, + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + ) + .then(res => res.json()) + .then( + ({ + data: { + currentUser: { features }, + }, + }) => { + if (features.includes(FeatureType.Admin)) { + toast.success('Logged in successfully'); + revalidate(); + } else { + toast.error('You are not an admin'); + } + } + ) + .catch(err => { + toast.error(`Failed to login: ${err.message}`); + }); + }, + [revalidate] + ); - useEffect(() => { - if (serverConfig.initialized === false) { - navigate('/admin/setup'); - return; - } else if (!currentUser) { - return; - } else if (!currentUser?.features.includes?.(FeatureType.Admin)) { - toast.error('You are not an admin, please login the admin account.'); - return; - } - }, [currentUser, navigate, serverConfig.initialized]); + if (currentUser && isAdmin(currentUser)) { + return ; + } return (
@@ -96,27 +86,36 @@ export function Auth() { Enter your email below to login to your account

-
-
- - -
-
-
- +
+
+
+ +
- +
+
+ +
+ +
+
- -
+
diff --git a/packages/frontend/admin/src/modules/common.ts b/packages/frontend/admin/src/modules/common.ts index 1c3b39c49f..a8f219db7e 100644 --- a/packages/frontend/admin/src/modules/common.ts +++ b/packages/frontend/admin/src/modules/common.ts @@ -1,6 +1,9 @@ +import { useMutateQueryResource } from '@affine/core/hooks/use-mutation'; import { useQueryImmutable } from '@affine/core/hooks/use-query'; +import type { GetCurrentUserFeaturesQuery } from '@affine/graphql'; import { adminServerConfigQuery, + FeatureType, getCurrentUserFeaturesQuery, } from '@affine/graphql'; @@ -12,10 +15,22 @@ export const useServerConfig = () => { return data.serverConfig; }; +export const useRevalidateCurrentUser = () => { + const revalidate = useMutateQueryResource(); + + return () => { + revalidate(getCurrentUserFeaturesQuery); + }; +}; export const useCurrentUser = () => { const { data } = useQueryImmutable({ query: getCurrentUserFeaturesQuery, }); - return data.currentUser; }; + +export function isAdmin( + user: NonNullable +) { + return user.features.includes(FeatureType.Admin); +} diff --git a/packages/frontend/admin/src/modules/config/index.tsx b/packages/frontend/admin/src/modules/config/index.tsx index 480a7a66a9..6a5772956e 100644 --- a/packages/frontend/admin/src/modules/config/index.tsx +++ b/packages/frontend/admin/src/modules/config/index.tsx @@ -9,7 +9,6 @@ import { Separator } from '@affine/admin/components/ui/separator'; import { useQueryImmutable } from '@affine/core/hooks/use-query'; import { getServerServiceConfigsQuery } from '@affine/graphql'; -import { Layout } from '../layout'; import { AboutAFFiNE } from './about'; type ServerConfig = { @@ -38,10 +37,6 @@ type ServerServiceConfig = { config: ServerConfig | MailerConfig | DatabaseConfig; }; -export function Config() { - return } />; -} - export function ConfigPage() { return (
@@ -218,4 +213,4 @@ export function ServerServiceConfig() { ); } -export { Config as Component }; +export { ConfigPage as Component }; diff --git a/packages/frontend/admin/src/modules/layout.tsx b/packages/frontend/admin/src/modules/layout.tsx index d2a0ba9b0d..28d1534683 100644 --- a/packages/frontend/admin/src/modules/layout.tsx +++ b/packages/frontend/admin/src/modules/layout.tsx @@ -6,10 +6,8 @@ import { import { Separator } from '@affine/admin/components/ui/separator'; import { TooltipProvider } from '@affine/admin/components/ui/tooltip'; import { cn } from '@affine/admin/utils'; -import { useQuery } from '@affine/core/hooks/use-query'; -import { FeatureType, getCurrentUserFeaturesQuery } from '@affine/graphql'; import { AlignJustifyIcon } from 'lucide-react'; -import type { ReactNode, RefObject } from 'react'; +import type { PropsWithChildren, ReactNode, RefObject } from 'react'; import { createContext, useCallback, @@ -19,8 +17,6 @@ import { useState, } from 'react'; import type { ImperativePanelHandle } from 'react-resizable-panels'; -import { useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; import { Button } from '../components/ui/button'; import { @@ -32,14 +28,9 @@ import { SheetTrigger, } from '../components/ui/sheet'; import { Logo } from './accounts/components/logo'; -import { useServerConfig } from './common'; import { NavContext } from './nav/context'; import { Nav } from './nav/nav'; -interface LayoutProps { - content: ReactNode; -} - interface RightPanelContextType { isOpen: boolean; rightPanelContent: ReactNode; @@ -81,14 +72,7 @@ export function useMediaQuery(query: string) { return value; } -export function Layout({ content }: LayoutProps) { - const serverConfig = useServerConfig(); - const { - data: { currentUser }, - } = useQuery({ - query: getCurrentUserFeaturesQuery, - }); - +export function Layout({ children }: PropsWithChildren) { const [rightPanelContent, setRightPanelContent] = useState(null); const [open, setOpen] = useState(false); const rightPanelRef = useRef(null); @@ -126,26 +110,6 @@ export function Layout({ content }: LayoutProps) { [closePanel, openPanel] ); - const navigate = useNavigate(); - - useEffect(() => { - if (serverConfig.initialized === false) { - navigate('/admin/setup'); - return; - } else if (!currentUser) { - navigate('/admin/auth'); - return; - } else if (!currentUser?.features.includes?.(FeatureType.Admin)) { - toast.error('You are not an admin, please login the admin account.'); - navigate('/admin/auth'); - return; - } - }, [currentUser, navigate, serverConfig.initialized]); - - if (serverConfig.initialized === false || !currentUser) { - return null; - } - return ( - {content} + {children} { - fetch('/api/auth/sign-out', { - method: 'POST', - }) + fetch('/api/auth/sign-out') .then(() => { toast.success('Logged out successfully'); - navigate('/admin/auth'); + relative(); }) .catch(err => { toast.error(`Failed to logout: ${err.message}`); }); - }, [navigate]); - - useEffect(() => { - if (serverConfig.initialized === false) { - navigate('/admin/setup'); - return; - } - if (!currentUser) { - navigate('/admin/auth'); - return; - } - if (!currentUser?.features.includes?.(FeatureType.Admin)) { - toast.error('You are not an admin, please login the admin account.'); - navigate('/admin/auth'); - return; - } - }, [currentUser, navigate, serverConfig.initialized]); + }, [relative]); return (
diff --git a/packages/frontend/admin/src/modules/settings/index.tsx b/packages/frontend/admin/src/modules/settings/index.tsx index 6aa25dd2f9..9d5770fa5d 100644 --- a/packages/frontend/admin/src/modules/settings/index.tsx +++ b/packages/frontend/admin/src/modules/settings/index.tsx @@ -6,7 +6,6 @@ import { CheckIcon } from 'lucide-react'; import type { Dispatch, SetStateAction } from 'react'; import { useCallback, useMemo, useState } from 'react'; -import { Layout } from '../layout'; import { useNav } from '../nav/context'; import { ConfirmChanges } from './confirm-changes'; import { RuntimeSettingRow } from './runtime-setting-row'; @@ -25,10 +24,6 @@ export type ModifiedValues = { newValue: any; }; -export function Settings() { - return } />; -} - export function SettingsPage() { const { trigger } = useUpdateServerRuntimeConfigs(); const { serverRuntimeConfig } = useGetServerRuntimeConfig(); @@ -190,4 +185,4 @@ export const AdminPanel = ({ ); }; -export { Settings as Component }; +export { SettingsPage as Component }; diff --git a/packages/frontend/admin/src/modules/setup/index.tsx b/packages/frontend/admin/src/modules/setup/index.tsx index 7bd1dbd1e2..dd41c459c9 100644 --- a/packages/frontend/admin/src/modules/setup/index.tsx +++ b/packages/frontend/admin/src/modules/setup/index.tsx @@ -1,7 +1,16 @@ +import { Navigate } from 'react-router-dom'; + +import { useServerConfig } from '../common'; import { Form } from './form'; import logo from './logo.svg'; export function Setup() { + const config = useServerConfig(); + + if (config.initialized) { + return ; + } + return (