diff --git a/AGENTS.md b/AGENTS.md index 59275882..6b4ae118 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,8 +22,8 @@ For example, prefer one state of type `'loading' | { error: E } | { result: T }` to three states `[loading, setLoading] = useState(); [error, setError] = useState(); [result, setResult] = useState()`. - For UI actions that hit the server, -prefer `useServerAction` (`@/lib/client/util`) or (if `useServerAction` doesn't work) `useActionState` -over manually storing response/error state with `useState`. + prefer `useServerAction` (`@/lib/client/util`) or (if `useServerAction` doesn't work) `useActionState` + over manually storing response/error state with `useState`. - To call Server Functions on mount (e.g. to fetch data), use SWR. - RSCs must call `await io()` before accessing server state (`getConfig`, `getAuth`, etc), unless they have already used a request-time API (`cookies`, `params`, etc). @@ -83,7 +83,37 @@ Some details/consequences: some of the Server Actions previously invoked over the network by the RCC may now be possible to call directly in the RSC; if no other RCC still needs these Server Actions, move them to the RSC file. - + +## Directory structure + +- Imports may refer to relative paths like `./actions` and `./SomeComponent`, + but imports like `../actions` or `../components/SomeComponent` are not permitted. +- Absolute imports starting with `@/app/components` are allowed; + no other imports starting with `@/app` are allowed. + (Example: `import { ErrorBox } from '@/app/components/ErrorBox'` is allowed, + `import { AdminErrorBox } from '@/app/components/admin/AdminErrorBox'` is allowed, + `import { SetupErrorBox } from '@/app/setup/SetupErrorBox'` is not allowed.) + +Actions will naturally start out in `actions.ts` files alongside the `page.tsx` file where they are used, +and components will naturally start out in `MyComponent.tsx` files alongside the `page.tsx` files where they are used; +however, as a consequence of these rules, +when the functionality is needed in a second location, actions or components may need to migrate to a new location. +Gathering components for a route into a subfolder (e.g. `./src/app/(settings)/admin/components`) +may also require actions to migrate into the `./src/lib` tree. + +- Functionality that is used in multiple places goes in `./src/app/components` and its subfolders (for `.tsx` files) + and in `./src/lib` and its subfolders (for server actions and other non-component `.ts` code). +- Most `.tsx` files that aren't a part of Next's routing should principally introduce a component, + along with hooks and helpers for that component. +- A file principally introducing the `ErrorBox` component should be named `ErrorBox.tsx`. +- Default exports should only be used where expected by Next.js. + A component `ErrorBox` should be exported as a non-default export in `ErrorBox.tsx`. + +## Linting + +- Autofixable problems should generally be `warn`, not `error`: + errors indicate only problems that aren't deterministically addressed by the pre-commit hook. + ## Git - Don't commit or push, let the user do that. You can automate git worktree operations, though. diff --git a/eslint.config.mjs b/eslint.config.mjs index bd3b5647..12962845 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -75,6 +75,17 @@ const eslintConfig = defineConfig([ 'warn', { allowSameFolder: true, rootDir: 'src', prefix: '@' }, ], + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + regex: '^@/app/(?!components/)', + message: 'Route folders are private; move shared code to @/app/components or @/lib.', + }, + ], + }, + ], }, }, { diff --git a/src/app/AvatarMenu.tsx b/src/app/AvatarMenu.tsx index 80391209..c7e2a1b5 100644 --- a/src/app/AvatarMenu.tsx +++ b/src/app/AvatarMenu.tsx @@ -3,14 +3,14 @@ import { usePathname, useRouter } from 'next/navigation' import { Button, Header, Menu, MenuItem, MenuSection, MenuTrigger, Popover } from 'react-aria-components' -import AvatarIcon from '@/app/components/AvatarIcon' -import MenuLinkItem from '@/app/components/MenuLinkItem' -import authClient, { githubSignin } from '@/lib/client/auth' +import { AvatarIcon } from '@/app/components/AvatarIcon' +import { MenuLinkItem } from '@/app/components/MenuLinkItem' +import { authClient, githubSignin } from '@/lib/client/auth' import { useThrowToBoundary } from '@/lib/client/util' import { useConfigCtx } from '@/lib/contexts' import { setIsAdmin } from '@/lib/server/actions' -export default function AvatarMenu() { +export function AvatarMenu() { const session = authClient.useSession() const cfg = useConfigCtx() const router = useRouter() diff --git a/src/app/Breadcrumbs.tsx b/src/app/Breadcrumbs.tsx index cec7d141..e5b60f2f 100644 --- a/src/app/Breadcrumbs.tsx +++ b/src/app/Breadcrumbs.tsx @@ -5,7 +5,7 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' import { Fragment } from 'react' -export default function Breadcrumbs() { +export function Breadcrumbs() { const pathname = usePathname() const segments = pathname.split('/').filter(c => c !== '') return ( diff --git a/src/app/[userName]/ProjectRow.tsx b/src/app/[userName]/ProjectRow.tsx index 18a21b90..1831f597 100644 --- a/src/app/[userName]/ProjectRow.tsx +++ b/src/app/[userName]/ProjectRow.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation' import { startTransition, useState } from 'react' import { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components' -import MenuLinkItem from '@/app/components/MenuLinkItem' +import { MenuLinkItem } from '@/app/components/MenuLinkItem' import { useServerAction } from '@/lib/client/util' import { deleteProject, type ProjectInfo, renameProject, toggleVisibility } from './actions' diff --git a/src/app/[userName]/[projectName]/publish/PublishCard.tsx b/src/app/[userName]/[projectName]/publish/PublishCard.tsx index caba084e..47d12614 100644 --- a/src/app/[userName]/[projectName]/publish/PublishCard.tsx +++ b/src/app/[userName]/[projectName]/publish/PublishCard.tsx @@ -3,8 +3,8 @@ import { useRouter } from 'next/navigation' import { startTransition } from 'react' -import ErrorBox from '@/app/components/ErrorBox' -import TrackedCommandForm from '@/app/components/TrackedCommandForm' +import { ErrorBox } from '@/app/components/ErrorBox' +import { TrackedCommandForm } from '@/app/components/TrackedCommandForm' import { useServerAction } from '@/lib/client/util' import { startPublishing, unpublish } from './actions' diff --git a/src/app/[userName]/[projectName]/publish/page.tsx b/src/app/[userName]/[projectName]/publish/page.tsx index 2f29a284..96dd3c56 100644 --- a/src/app/[userName]/[projectName]/publish/page.tsx +++ b/src/app/[userName]/[projectName]/publish/page.tsx @@ -4,7 +4,7 @@ import { notFound } from 'next/navigation' import { connection } from 'next/server' import z from 'zod' -import ErrorBox from '@/app/components/ErrorBox' +import { ErrorBox } from '@/app/components/ErrorBox' import { detectPublishable, PUBLISH_MANIFEST_FILE } from '@/lib/server/artifacts' import { getConfig, isPublishingEnabled } from '@/lib/server/config' import { getDb } from '@/lib/server/db' diff --git a/src/app/admin/components/AllowlistEditor.tsx b/src/app/admin/components/AllowlistEditor.tsx index 19911de3..65cf3649 100644 --- a/src/app/admin/components/AllowlistEditor.tsx +++ b/src/app/admin/components/AllowlistEditor.tsx @@ -3,8 +3,8 @@ import { useRouter } from 'next/navigation' import { startTransition, useRef } from 'react' -import { addAllowedUser, removeAllowedUser } from '@/app/admin/actions' import { useServerAction } from '@/lib/client/util' +import { addAllowedUser, removeAllowedUser } from '@/lib/server/adminActions' export function AllowlistEditor({ users }: { users: string[] }) { const router = useRouter() diff --git a/src/app/admin/components/HealthMonitor.tsx b/src/app/admin/components/HealthMonitor.tsx index e7c67c1f..fcef850e 100644 --- a/src/app/admin/components/HealthMonitor.tsx +++ b/src/app/admin/components/HealthMonitor.tsx @@ -2,9 +2,9 @@ import { startTransition, use, useState } from 'react' -import { fetchDiskUsage, fetchHealth, type SystemHealth } from '@/app/admin/actions' -import CatchySuspense from '@/app/components/CatchySuspense' +import { CatchySuspense } from '@/app/components/CatchySuspense' import { useServerAction, useThrowingSWR } from '@/lib/client/util' +import { fetchDiskUsage, fetchHealth, type SystemHealth } from '@/lib/server/adminActions' import { formatBytes, formatUptime } from './utils' diff --git a/src/app/admin/components/OAuthConfig.tsx b/src/app/admin/components/OAuthConfig.tsx index a18b47d8..9c500be0 100644 --- a/src/app/admin/components/OAuthConfig.tsx +++ b/src/app/admin/components/OAuthConfig.tsx @@ -3,9 +3,9 @@ import { useRouter } from 'next/navigation' import { use, useState } from 'react' -import { updateOAuthConfig } from '@/app/admin/actions' -import CatchySuspense from '@/app/components/CatchySuspense' +import { CatchySuspense } from '@/app/components/CatchySuspense' import { useServerAction } from '@/lib/client/util' +import { updateOAuthConfig } from '@/lib/server/adminActions' interface OAuthConfigProps { oauthConfigPromise: Promise<{ clientId: string }> diff --git a/src/app/admin/components/RegistrationModeControl.tsx b/src/app/admin/components/RegistrationModeControl.tsx index fb176bb6..d243ef18 100644 --- a/src/app/admin/components/RegistrationModeControl.tsx +++ b/src/app/admin/components/RegistrationModeControl.tsx @@ -3,8 +3,8 @@ import { useRouter } from 'next/navigation' import { startTransition, useState } from 'react' -import { setRegistrationMode } from '@/app/admin/actions' import { useServerAction } from '@/lib/client/util' +import { setRegistrationMode } from '@/lib/server/adminActions' import type { RegistrationMode } from '@/lib/server/config' const MODES: { value: RegistrationMode; label: string }[] = [ diff --git a/src/app/admin/components/SessionRow.tsx b/src/app/admin/components/SessionRow.tsx index c82548ea..180a231b 100644 --- a/src/app/admin/components/SessionRow.tsx +++ b/src/app/admin/components/SessionRow.tsx @@ -3,9 +3,9 @@ import { useRouter } from 'next/navigation' import { startTransition } from 'react' -import { killEditorSession } from '@/app/admin/actions' -import ProjectLink from '@/app/components/ProjectLink' +import { ProjectLink } from '@/app/components/ProjectLink' import { useServerAction } from '@/lib/client/util' +import { killEditorSession } from '@/lib/server/adminActions' import type { EditorSessionInfo, UnknownEditorSession } from '@/lib/server/shardConnection' export function SessionRow({ info }: { info: EditorSessionInfo | UnknownEditorSession }) { diff --git a/src/app/admin/components/SessionViewer.tsx b/src/app/admin/components/SessionViewer.tsx index 56451167..a1a873c9 100644 --- a/src/app/admin/components/SessionViewer.tsx +++ b/src/app/admin/components/SessionViewer.tsx @@ -1,6 +1,6 @@ import { io } from 'next/cache' -import CatchySuspense from '@/app/components/CatchySuspense' +import { CatchySuspense } from '@/app/components/CatchySuspense' import { requireAdmin } from '@/lib/server/auth' import { getShardCoordinator } from '@/lib/server/shardConnection' diff --git a/src/app/admin/components/TemplateManagement.tsx b/src/app/admin/components/TemplateManagement.tsx index ce7a2a3f..45131319 100644 --- a/src/app/admin/components/TemplateManagement.tsx +++ b/src/app/admin/components/TemplateManagement.tsx @@ -8,12 +8,12 @@ import { startTransition, use, useState } from 'react' import { Button, Dialog, DialogTrigger, Heading, Modal } from 'react-aria-components' import { type Key, Tab, TabList, TabPanel, TabPanels, Tabs } from 'react-aria-components' -import { availableTemplateSchemas, doTemplateCreation, editTemplateMetadata } from '@/app/admin/actions' -import CatchySuspense from '@/app/components/CatchySuspense' -import ErrorBox from '@/app/components/ErrorBox' +import { CatchySuspense } from '@/app/components/CatchySuspense' +import { ErrorBox } from '@/app/components/ErrorBox' import { QuickFilterInput, useQuickFilter } from '@/app/components/QuickFilterInput' -import TrackedCommandForm from '@/app/components/TrackedCommandForm' +import { TrackedCommandForm } from '@/app/components/TrackedCommandForm' import { useServerAction, useThrowingSWR } from '@/lib/client/util' +import { availableTemplateSchemas, doTemplateCreation, editTemplateMetadata } from '@/lib/server/adminActions' import { type TemplateInfo } from '@/lib/server/projectTemplate' interface TemplateManagementProps { diff --git a/src/app/admin/components/ToolchainManagement.tsx b/src/app/admin/components/ToolchainManagement.tsx index 375d6f28..c0d48f89 100644 --- a/src/app/admin/components/ToolchainManagement.tsx +++ b/src/app/admin/components/ToolchainManagement.tsx @@ -10,11 +10,11 @@ import { useRouter } from 'next/navigation' import { use, useState } from 'react' import z from 'zod' -import { doElanInstall, uninstallToolchainVersion } from '@/app/admin/actions' -import CatchySuspense from '@/app/components/CatchySuspense' +import { CatchySuspense } from '@/app/components/CatchySuspense' import { QuickFilterInput, useQuickFilter } from '@/app/components/QuickFilterInput' -import TrackedCommandForm from '@/app/components/TrackedCommandForm' +import { TrackedCommandForm } from '@/app/components/TrackedCommandForm' import { useServerAction, useThrowingSWR } from '@/lib/client/util' +import { doElanInstall, uninstallToolchainVersion } from '@/lib/server/adminActions' interface ToolchainManagementProps { installedToolchainsPromise: Promise diff --git a/src/app/admin/components/UserRow.tsx b/src/app/admin/components/UserRow.tsx index 049203ad..77cac217 100644 --- a/src/app/admin/components/UserRow.tsx +++ b/src/app/admin/components/UserRow.tsx @@ -3,8 +3,8 @@ import { useRouter } from 'next/navigation' import { startTransition } from 'react' -import { deleteUser, toggleAdmin } from '@/app/admin/actions' import { useServerAction } from '@/lib/client/util' +import { deleteUser, toggleAdmin } from '@/lib/server/adminActions' import { type User } from '@/lib/server/auth' export function UserRow({ user, isSelf }: { user: Pick; isSelf: boolean }) { diff --git a/src/app/admin/forbidden.tsx b/src/app/admin/forbidden.tsx index 0712afdc..9b3595fd 100644 --- a/src/app/admin/forbidden.tsx +++ b/src/app/admin/forbidden.tsx @@ -1,4 +1,4 @@ -import ErrorBox from '@/app/components/ErrorBox' +import { ErrorBox } from '@/app/components/ErrorBox' export default function Forbidden() { return ( diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index d0b5ea44..1cddc4ea 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,10 +1,10 @@ -import CatchySuspense from '@/app/components/CatchySuspense' +import { CatchySuspense } from '@/app/components/CatchySuspense' +import { fetchHealth } from '@/lib/server/adminActions' import { getOAuthConfig, requireAdmin } from '@/lib/server/auth' import { getConfig } from '@/lib/server/config' import { listInstalledToolchains } from '@/lib/server/elan' import { listTemplates } from '@/lib/server/projectTemplate' -import { fetchHealth } from './actions' import { AccessControl } from './components/AccessControl' import { HealthMonitor } from './components/HealthMonitor' import { OAuthConfig } from './components/OAuthConfig' diff --git a/src/app/admin/unauthorized.tsx b/src/app/admin/unauthorized.tsx index 00af398c..98fb8ab3 100644 --- a/src/app/admin/unauthorized.tsx +++ b/src/app/admin/unauthorized.tsx @@ -1,4 +1,4 @@ -import AdminLogin from './components/AdminLogin' +import { AdminLogin } from '@/app/components/AdminLogin' export default function Unauthorized() { return diff --git a/src/app/admin/components/AdminLogin.tsx b/src/app/components/AdminLogin.tsx similarity index 90% rename from src/app/admin/components/AdminLogin.tsx rename to src/app/components/AdminLogin.tsx index 50e5d8ef..4561bd06 100644 --- a/src/app/admin/components/AdminLogin.tsx +++ b/src/app/components/AdminLogin.tsx @@ -4,12 +4,12 @@ import { adminEmail, MIN_ADMIN_PASSWORD_LENGTH } from '@leanprover/workbench-sha import { useActionState } from 'react' import z from 'zod' -import auth from '@/lib/client/auth' +import { authClient } from '@/lib/client/auth' async function loginAction(_: string | null, formData: FormData) { const password = String(formData.get('password') ?? '') try { - await auth.signIn.email({ email: adminEmail, password }) + await authClient.signIn.email({ email: adminEmail, password }) } catch (e) { if (z.object({ error: z.object({ code: z.literal('INVALID_EMAIL_OR_PASSWORD') }) }).safeParse(e).success) { return 'Incorrect password' @@ -21,7 +21,7 @@ async function loginAction(_: string | null, formData: FormData) { return null } -export default function AdminLogin() { +export function AdminLogin() { const [error, action, isPending] = useActionState(loginAction, null) return ( diff --git a/src/app/components/AvatarIcon.tsx b/src/app/components/AvatarIcon.tsx index 3d5b0944..67ae60f2 100644 --- a/src/app/components/AvatarIcon.tsx +++ b/src/app/components/AvatarIcon.tsx @@ -2,7 +2,7 @@ import Image from 'next/image' import type { User } from '@/lib/server/auth' -export default function AvatarIcon({ user }: { user: Pick }) { +export function AvatarIcon({ user }: { user: Pick }) { return ( {user.image ? ( diff --git a/src/app/components/CatchySuspense.tsx b/src/app/components/CatchySuspense.tsx index 9c2eed81..f3e82702 100644 --- a/src/app/components/CatchySuspense.tsx +++ b/src/app/components/CatchySuspense.tsx @@ -4,7 +4,7 @@ import { catchError, type ErrorInfo } from 'next/error' import { type ReactNode, Suspense } from 'react' import z from 'zod' -import ErrorBox from './ErrorBox' +import { ErrorBox } from './ErrorBox' function ErrorFallback(props: { fallback?: ReactNode | ((err: unknown) => ReactNode) }, { error, retry }: ErrorInfo) { let displayedError: ReactNode @@ -54,7 +54,7 @@ const ErrorBoundary = catchError(ErrorFallback) * Keep in mind that, in production environments, the actual thrown error may originate from the server * and so will be an unhelpful digest string with a generic "An error occurred in the Server Components blah blah blah" message. */ -export default function CatchySuspense(props: { +export function CatchySuspense(props: { loading: ReactNode error?: ReactNode | ((err: unknown) => ReactNode) children: ReactNode diff --git a/src/app/components/ErrorBox.tsx b/src/app/components/ErrorBox.tsx index d5f624df..a6496625 100644 --- a/src/app/components/ErrorBox.tsx +++ b/src/app/components/ErrorBox.tsx @@ -1,4 +1,4 @@ -export default function ErrorBox({ children }: { children: React.ReactNode }) { +export function ErrorBox({ children }: { children: React.ReactNode }) { return (
({ +export function MenuLinkItem({ href, ...props }: Omit & { href: Route }) { diff --git a/src/app/components/ProjectLink.tsx b/src/app/components/ProjectLink.tsx index 46e70c9a..597a7687 100644 --- a/src/app/components/ProjectLink.tsx +++ b/src/app/components/ProjectLink.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' -export default function ProjectLink({ ownerUsername, projectName }: { ownerUsername: string; projectName: string }) { +export function ProjectLink({ ownerUsername, projectName }: { ownerUsername: string; projectName: string }) { return ( <> {ownerUsername} diff --git a/src/app/components/SimpleTTY.tsx b/src/app/components/SimpleTTY.tsx index b8d75698..3f1a6c57 100644 --- a/src/app/components/SimpleTTY.tsx +++ b/src/app/components/SimpleTTY.tsx @@ -38,7 +38,7 @@ interface SimpleTTYProps { * The auto-reconnection features of SSE are disabled; instead, an explicit * user action is needed to reconnect. */ -export default function SimpleTTY(props: SimpleTTYProps) { +export function SimpleTTY(props: SimpleTTYProps) { const [attempt, setAttempt] = useState(0) return setAttempt(attempt => attempt + 1)} {...props} /> } diff --git a/src/app/components/TrackedCommandForm.tsx b/src/app/components/TrackedCommandForm.tsx index 433d1eb9..7cdf9d17 100644 --- a/src/app/components/TrackedCommandForm.tsx +++ b/src/app/components/TrackedCommandForm.tsx @@ -2,8 +2,8 @@ import { type CSSProperties, type ReactNode, startTransition, useCallback, useEffect, useState } from 'react' -import ErrorBox from '@/app/components/ErrorBox' -import SimpleTTY from '@/app/components/SimpleTTY' +import { ErrorBox } from '@/app/components/ErrorBox' +import { SimpleTTY } from '@/app/components/SimpleTTY' import { useServerAction, useThrowToBoundary } from '@/lib/client/util' import { isTrackedCommandAvailable, isTrackedCommandRunning } from '@/lib/server/actions' import { type ActionResponse, type TrackedCommandExit, type TrackedCommandScope } from '@/lib/util' @@ -49,7 +49,7 @@ type FormState = * "Close" button will be replaced (or augmented, if `successButton.allowClose` is true) with * a button that contains `successButton.text` and performs `successButton.action` on click. */ -export default function TrackedCommandForm({ +export function TrackedCommandForm({ streamCommandKey, scope, style, diff --git a/src/app/error.tsx b/src/app/error.tsx index 21725d48..ae1843a6 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react' import { useSWRConfig } from 'swr' -import ErrorBox from './components/ErrorBox' +import { ErrorBox } from '@/app/components/ErrorBox' interface ErrorBoundaryProps { error: Error & { digest?: string } diff --git a/src/app/forbidden.tsx b/src/app/forbidden.tsx index cdfa3c7d..71a248ba 100644 --- a/src/app/forbidden.tsx +++ b/src/app/forbidden.tsx @@ -1,4 +1,4 @@ -import ErrorBox from './components/ErrorBox' +import { ErrorBox } from '@/app/components/ErrorBox' export default function Forbidden() { return ( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 64ef144e..9172efaf 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,8 +8,8 @@ import { type ReactNode, Suspense } from 'react' import { ConfigCtxProvider } from '@/app/components/ConfigCtxProvider' -import AvatarMenu from './AvatarMenu' -import Breadcrumbs from './Breadcrumbs' +import { AvatarMenu } from './AvatarMenu' +import { Breadcrumbs } from './Breadcrumbs' import { NavbarExtra, NavbarExtraProvider } from './NavbarExtra' const openSans = Open_Sans({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 7fb60e45..082093a4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,13 +4,12 @@ import { type Route } from 'next' import Link from 'next/link' import { useSearchParams } from 'next/navigation' -import authClient, { githubSignin } from '@/lib/client/auth' +import { ErrorBox } from '@/app/components/ErrorBox' +import { authClient, githubSignin } from '@/lib/client/auth' import { useServerAction, useThrowToBoundary } from '@/lib/client/util' import { type Config, useConfigCtx } from '@/lib/contexts' import { loginDevUser } from '@/lib/server/actions' -import ErrorBox from './components/ErrorBox' - /** * Interprets an (untrusted) `error` search parameter as OAuth callback information */ diff --git a/src/app/setup/SetupAdmin.tsx b/src/app/setup/SetupAdmin.tsx index a8f08616..78379984 100644 --- a/src/app/setup/SetupAdmin.tsx +++ b/src/app/setup/SetupAdmin.tsx @@ -3,22 +3,22 @@ import { adminEmail, MIN_ADMIN_PASSWORD_LENGTH } from '@leanprover/workbench-shared' import { useActionState, useState } from 'react' -import auth from '@/lib/client/auth' +import { authClient } from '@/lib/client/auth' async function firstLoginAction(_: string | null, formData: FormData) { const currentPassword = String(formData.get('currentPassword') ?? '') const newPassword = String(formData.get('newPassword') ?? '') try { - await auth.signIn.email({ email: adminEmail, password: currentPassword }) + await authClient.signIn.email({ email: adminEmail, password: currentPassword }) } catch (e) { return 'Incorrect password' } try { // Technically a write-after-read race condition (currentPassword could have changed!). It's fine. - await auth.changePassword({ currentPassword, newPassword, revokeOtherSessions: true }) + await authClient.changePassword({ currentPassword, newPassword, revokeOtherSessions: true }) } catch (e) { - await auth.signOut() + await authClient.signOut() throw e } @@ -26,7 +26,7 @@ async function firstLoginAction(_: string | null, formData: FormData) { return null } -export default function SetupAdmin() { +export function SetupAdmin() { const [newPassword, setNewPassword] = useState('') const [confirmNewPassword, setConfirmNewPassword] = useState('') const newPasswordsMatch = diff --git a/src/app/setup/SetupFlow.tsx b/src/app/setup/SetupFlow.tsx index 18fc752f..1ee0b419 100644 --- a/src/app/setup/SetupFlow.tsx +++ b/src/app/setup/SetupFlow.tsx @@ -3,7 +3,7 @@ import { redirect, useRouter } from 'next/navigation' import { useState, useSyncExternalStore } from 'react' -import TrackedCommandForm from '@/app/components/TrackedCommandForm' +import { TrackedCommandForm } from '@/app/components/TrackedCommandForm' import { useConfigCtx } from '@/lib/contexts' import { doSeed } from './actions' @@ -12,7 +12,7 @@ interface SetupFlowProps { baseUrl: string } -export default function SetupFlow({ baseUrl }: SetupFlowProps) { +export function SetupFlow({ baseUrl }: SetupFlowProps) { const router = useRouter() const cfg = useConfigCtx() const [wasCompleteOnMount] = useState(cfg.isSetupComplete) diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 57c89e81..fe4201fb 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -1,7 +1,7 @@ import { requireAdmin } from '@/lib/server/auth' import { getConfig } from '@/lib/server/config' -import SetupFlow from './SetupFlow' +import { SetupFlow } from './SetupFlow' export const instant = false diff --git a/src/app/setup/unauthorized.tsx b/src/app/setup/unauthorized.tsx index 235dc6f0..747c5ff2 100644 --- a/src/app/setup/unauthorized.tsx +++ b/src/app/setup/unauthorized.tsx @@ -2,10 +2,10 @@ import 'server-only' import { io } from 'next/cache' -import AdminLogin from '@/app/admin/components/AdminLogin' +import { AdminLogin } from '@/app/components/AdminLogin' import { getConfig } from '@/lib/server/config' -import SetupAdmin from './SetupAdmin' +import { SetupAdmin } from './SetupAdmin' export default async function Unauthorized() { await io() diff --git a/src/app/unauthorized.tsx b/src/app/unauthorized.tsx index d3ceeaac..07f32593 100644 --- a/src/app/unauthorized.tsx +++ b/src/app/unauthorized.tsx @@ -1,4 +1,4 @@ -import ErrorBox from './components/ErrorBox' +import { ErrorBox } from '@/app/components/ErrorBox' export default function Unauthorized() { return ( diff --git a/src/lib/client/auth.ts b/src/lib/client/auth.ts index 8b096b13..517cb3f8 100644 --- a/src/lib/client/auth.ts +++ b/src/lib/client/auth.ts @@ -3,7 +3,7 @@ import { createAuthClient } from 'better-auth/react' import type { AuthInstance } from '@/lib/server/auth' -const authClient = createAuthClient({ +export const authClient = createAuthClient({ // connection errors always throw, this makes non-ok status throw as well so they can be handled similarly fetchOptions: { throw: true }, // https://better-auth.com/docs/concepts/typescript#inferring-additional-fields-on-client @@ -16,5 +16,3 @@ export function githubSignin() { callbackURL: window.location.pathname, }) } - -export default authClient diff --git a/src/app/admin/actions.ts b/src/lib/server/adminActions.ts similarity index 100% rename from src/app/admin/actions.ts rename to src/lib/server/adminActions.ts