Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
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).

# Error handling

Expand Down
3 changes: 3 additions & 0 deletions src/app/admin/components/AccessControl.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { io } from 'next/cache'

import { getConfig } from '@/lib/server/config'
import { getDb } from '@/lib/server/db'

import { AllowlistEditor } from './AllowlistEditor'
import { RegistrationModeControl } from './RegistrationModeControl'

export async function AccessControl() {
await io()
const mode = getConfig().registrationMode
const allowed =
mode === 'restricted' ? await getDb().allowedGithubUser.findMany({ orderBy: { githubUsername: 'asc' } }) : []
Expand Down
6 changes: 5 additions & 1 deletion src/app/admin/components/UserManagement.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { io } from 'next/cache'

import { requireAdmin } from '@/app/admin/actions'
import { getDb } from '@/lib/server/db'

import { UserRow } from './UserRow'

export async function UserManagement() {
await io()
const db = getDb()
const [users, session] = await Promise.all([
getDb().user.findMany({
db.user.findMany({
select: { id: true, name: true, isAdmin: true },
orderBy: { createdAt: 'asc' },
}),
Expand Down
14 changes: 7 additions & 7 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import '@/css/app.css'

import type { Metadata } from 'next'
import { io } from 'next/cache'
import { Open_Sans } from 'next/font/google'
import Image from 'next/image'
import Link from 'next/link'
import { connection } from 'next/server'
import { type ReactNode, Suspense } from 'react'

import { ConfigCtx } from '@/lib/contexts'
Expand All @@ -30,7 +30,8 @@ export default async function RootLayout({
}: Readonly<{
children: ReactNode
}>) {
// https://nextjs.org/docs/app/getting-started/caching#opting-out-of-the-static-shell
// FIXME: this is currently the only `Suspense` in the app,
// so we don't get static shells for any page.
return (
<Suspense fallback={null}>
<RootLayoutBody>{children}</RootLayoutBody>
Expand All @@ -43,11 +44,10 @@ async function RootLayoutBody({
}: Readonly<{
children: ReactNode
}>) {
// NOTE: together with the Suspense above, this makes the entire app dynamic:
// nothing is pre-rendered and every request recomputes its output HTML.
// This means we don't have to worry about cache invalidation,
// but will need fixing if it becomes a perf issue.
await connection()
// NOTE: Awaiting `io` makes the root layout dynamic
// (every request recomputes its output HTML),
// though static shells for pages may still be pre-rendered by `next build`.
await io()

const serverCfg = getConfig()
const clientCfg = {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { betterAuth, type SocialProviders } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { nextCookies } from 'better-auth/next-js'
import crypto from 'crypto'
import { io } from 'next/cache'
import { headers } from 'next/headers'
import { unauthorized } from 'next/navigation'

Expand Down Expand Up @@ -134,6 +135,7 @@ export async function getAuth(): Promise<AuthInstance> {

/** Require an authenticated user. Throws `unauthorized()` if not logged in. */
export async function requireAuth(): Promise<SessionAndUser> {
await io()
const auth = await getAuth()
const session = await auth.api.getSession({ headers: await headers() })
if (!session) unauthorized()
Expand Down
4 changes: 1 addition & 3 deletions src/lib/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ function ensureConfigWatcher() {

/** Return the server configuration.
*
* The object may be mutated. `saveConfig()` must be called after any modifications.
*
* Use {@link CacheTag.serverConfig} in Server Components that read this. */
* The object may be mutated. `saveConfig()` must be called after any modifications. */
export function getConfig(): ServerConfig {
if (!g.__config) initConfig()
return g.__config!
Expand Down
Loading