Skip to content

Commit

Permalink
feature: Nextjs 13 App Directory Utility Methods (#4103)
Browse files Browse the repository at this point in the history
* working

* fix toolkit app

* fix add directive

* add back secure password

* changeset

* improve blitz rpc structure

* make it work

* fix most integration tests

* fix the tests

* fix import error

* fix unit test

* fix tests

* remove breaking changes

* remove another breaking change

* fix typescript declaration

* fix errorboundary redirect issues

* cleanup

* upgrade to next13

* update pnpm-lock

* add Server Plugin Export

* fix lint error

* fix build errors

* fixes

* Solve NextRouter is not mounted

* fix

* fix issues

* pnpm lock fix

* rename session function and add auth utility

* remove potential implicit warning

* fix deps and cleanup

* fix secure-password missing file

* fix possibility of crsf token cookie being null

* remove secure-password breaking changes

* Update cuddly-singers-perform.md

* rename functions and remove headers

* Revert "remove secure-password breaking changes"

This reverts commit abccf93.

* unsupported auth methods in react server components

* remove authorizedContext: To be done in another PR

* add integration test for app dir

* fix duplicate package name

* Revert "remove authorizedContext: To be done in another PR"

This reverts commit 46fe8af.

* pnpm lock

* integration test

* remove empty then and add void operator

* add new invokeResolver function

* fix bug in useAuthenticatedBlitzContext

* Create .changeset/tall-radios-clean.md

* overload the invoke function for the new RSC usecase

* invoke works without implicit passing of arguments

* fix pnpm lock
  • Loading branch information
siddhsuresh authored Mar 29, 2023
1 parent cadefb8 commit 37aeaa7
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 20 deletions.
64 changes: 64 additions & 0 deletions .changeset/tall-radios-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
"next-blitz-auth": patch
"@blitzjs/auth": patch
"@blitzjs/rpc": patch
"blitz": patch
---

feature: Nextjs 13 App Directory Utility Methods

### 🔧 New Blitz Auth Hook `useAuthenticatedBlitzContext`

This hook is implemented as the replacement of the [`BlitzPage` seurity auth utilities](https://blitzjs.com/docs/authorization#secure-your-pages) provided for the pages directory to work with React Server Components in the Nextjs 13 app directory
It can be used in any asynchronous server component be it in `page.ts` or in the layouts in `layout.ts`
It uses the new [`redirect` function](https://beta.nextjs.org/docs/api-reference/redirect) to provide the required authorization in server side

#### API
```ts
useAuthenticatedBlitzContext({
redirectTo,
redirectAuthenticatedTo,
role,
}: {
redirectTo?: string | RouteUrlObject
redirectAuthenticatedTo?: string | RouteUrlObject | ((ctx: Ctx) => string | RouteUrlObject)
role?: string | string[]
}): Promise<void>
```

#### Usage
**Example Usage in React Server Component in `app` directory in Next 13**
```ts
import {getAppSession, useAuthenticatedBlitzContext} from "src/blitz-server"
...
await useAuthenticatedBlitzContext({
redirectTo: "/auth/login",
role: ["admin"],
redirectAuthenticatedTo: "/dashboard",
})
```

### 🔧 New Blitz RPC Hook `invokeResolver`

#### API
```ts
invokeResolver<T extends (...args: any) => any, TInput = FirstParam<T>>(
queryFn: T,
params: TInput,
): Promise<PromiseReturnType<T>>
```

#### Example Usage

```ts
...
import {invokeResolver, useAuthenticatedBlitzContext} from "../src/blitz-server"
import getCurrentUser from "../src/users/queries/getCurrentUser"

export default async function Home() {
await useAuthenticatedBlitzContext({
redirectTo: "/auth/login",
})
const user = await invokeResolver(getCurrentUser, null)
...
```
8 changes: 8 additions & 0 deletions apps/next13/app/auth/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {useAuthenticatedBlitzContext} from "@blitzjs/auth"

export default async function RootLayout({children}: {children: React.ReactNode}) {
await useAuthenticatedBlitzContext({
redirectAuthenticatedTo: "/",
})
return <>{children}</>
}
11 changes: 1 addition & 10 deletions apps/next13/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,7 @@ import {useSearchParams} from "next/navigation"
const LoginPage = () => {
const router = useRouter()
const searchParams = useSearchParams()
return (
<LoginForm
onSuccess={(_user) => {
const next = searchParams.get("next")
? decodeURIComponent(searchParams.get("next") as string)
: "/"
return router.push(next)
}}
/>
)
return <LoginForm onSuccess={(_user) => {}} />
}

export default LoginPage
10 changes: 5 additions & 5 deletions apps/next13/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Link from "next/link"
import styles from "src/styles/Home.module.css"
import Test from "./react-query"
import {getBlitzContext} from "../src/blitz-server"
import {invoke, useAuthenticatedBlitzContext} from "../src/blitz-server"
import getCurrentUser from "../src/users/queries/getCurrentUser"

export default async function Home() {
const ctx = await getBlitzContext()
ctx.session.$create({userId: 1})
console.log("session", ctx.session.userId)
const user = await getCurrentUser(null, ctx)
await useAuthenticatedBlitzContext({
redirectTo: "/auth/login",
})
const user = await invoke(getCurrentUser, null)
console.log("user", user)
return (
<div
Expand Down
6 changes: 4 additions & 2 deletions apps/next13/src/blitz-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import {AuthServerPlugin, PrismaStorage} from "@blitzjs/auth"
import db from "../prisma"
import {simpleRolesIsAuthorized} from "@blitzjs/auth"
import {BlitzLogger} from "blitz"
import {RpcServerPlugin} from "@blitzjs/rpc"

const {api, getBlitzContext} = setupBlitzServer({
const {api, getBlitzContext, useAuthenticatedBlitzContext, invoke} = setupBlitzServer({
plugins: [
AuthServerPlugin({
cookiePrefix: "web-cookie-prefix",
storage: PrismaStorage(db),
isAuthorized: simpleRolesIsAuthorized,
}),
RpcServerPlugin({}),
],
logger: BlitzLogger({}),
})

export {api, getBlitzContext}
export {api, getBlitzContext, useAuthenticatedBlitzContext, invoke}

export const cliConfig: BlitzCliConfig = {
customTemplates: "src/templates",
Expand Down
3 changes: 2 additions & 1 deletion packages/blitz-auth/src/server/auth-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {BlitzServerPlugin, RequestMiddleware, Ctx, createServerPlugin} from "bli
import {assert} from "blitz"
import {IncomingMessage, ServerResponse} from "http"
import {PublicData, SessionModel, SessionConfigMethods} from "../shared/types"
import {getBlitzContext, getSession} from "./auth-sessions"
import {getBlitzContext, getSession, useAuthenticatedBlitzContext} from "./auth-sessions"

interface SessionConfigOptions {
cookiePrefix?: string
Expand Down Expand Up @@ -129,6 +129,7 @@ export const AuthServerPlugin = createServerPlugin((options: AuthPluginOptions)
requestMiddlewares: [authPluginSessionMiddleware()],
exports: () => ({
getBlitzContext,
useAuthenticatedBlitzContext,
}),
}
})
61 changes: 61 additions & 0 deletions packages/blitz-auth/src/server/auth-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
AuthorizationError,
CSRFTokenMismatchError,
log,
baseLogger,
chalk,
} from "blitz"
import {
EmptyPublicData,
Expand Down Expand Up @@ -209,6 +211,65 @@ interface RouteUrlObject extends Pick<UrlObject, "pathname" | "query" | "href">
pathname: string
}

export async function useAuthenticatedBlitzContext({
redirectTo,
redirectAuthenticatedTo,
role,
}: {
redirectTo?: string | RouteUrlObject
redirectAuthenticatedTo?: string | RouteUrlObject | ((ctx: Ctx) => string | RouteUrlObject)
role?: string | string[]
}): Promise<void> {
const log = baseLogger().getChildLogger()
const customChalk = new chalk.Instance({
level: log.settings.type === "json" ? 0 : chalk.level,
})
const ctx: Ctx = await getBlitzContext()
const userId = ctx.session.userId
const {redirect} = await import("next/navigation").catch(() => {
throw new Error(
"useAuthenticatedBlitzContext() can only be used in React Server Components in Nextjs 13 or higher",
)
})
if (userId) {
debug("[useAuthenticatedBlitzContext] User is authenticated")
if (redirectAuthenticatedTo) {
if (typeof redirectAuthenticatedTo === "function") {
redirectAuthenticatedTo = redirectAuthenticatedTo(ctx)
}
const redirectUrl =
typeof redirectAuthenticatedTo === "string"
? redirectAuthenticatedTo
: formatWithValidation(redirectAuthenticatedTo)
debug("[useAuthenticatedBlitzContext] Redirecting to", redirectUrl)
log.info("Authentication Redirect: " + customChalk.dim("(Authenticated)"), redirectUrl)
redirect(redirectUrl)
}
if (redirectTo && role) {
debug("[useAuthenticatedBlitzContext] redirectTo and role are both defined.")
try {
ctx.session.$authorize(role)
} catch (e) {
log.error("Authorization Error: " + (e as Error).message)
if (typeof redirectTo !== "string") {
redirectTo = formatWithValidation(redirectTo)
}
log.info("Authorization Redirect: " + customChalk.dim(`Role ${role}`), redirectTo)
redirect(redirectTo)
}
}
} else {
debug("[useAuthenticatedBlitzContext] User is not authenticated")
if (redirectTo) {
if (typeof redirectTo !== "string") {
redirectTo = formatWithValidation(redirectTo)
}
log.info("Authentication Redirect: " + customChalk.dim("(Not authenticated)"), redirectTo)
redirect(redirectTo)
}
}
}

const makeProxyToPublicData = <T extends SessionContextClass>(ctxClass: T): T => {
return new Proxy(ctxClass, {
get(target, prop, receiver) {
Expand Down
24 changes: 22 additions & 2 deletions packages/blitz-rpc/src/client/invoke.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import {FirstParam, PromiseReturnType, isClient, Ctx} from "blitz"
import {RpcClient} from "./rpc"

export function invoke<T extends (...args: any) => any, TInput = FirstParam<T>>(
export async function invoke<T extends (...args: any) => any, TInput = FirstParam<T>>(
queryFn: T,
params: TInput,
): Promise<PromiseReturnType<T>> {
): Promise<T>
export async function invoke<T extends (...args: any) => any, TInput = FirstParam<T>>(
queryFn: T,
params: TInput,
isServer: boolean,
): Promise<T>
export async function invoke<T extends (...args: any) => any, TInput = FirstParam<T>>(
queryFn: T,
params: TInput,
isServer = typeof window === "undefined" ? true : false,
): Promise<T> {
if (typeof queryFn === "undefined") {
throw new Error(
"invoke is missing the first argument - it must be a query or mutation function",
)
}

if (isServer) {
const {getBlitzContext} = await import("@blitzjs/auth").catch((e) => {
throw new Error(
`invoke with isServer parameter can only be used in a Blitz powered Nextjs app directory. Make sure you have installed the @blitzjs/auth package.`,
)
})
const ctx = await getBlitzContext()
return queryFn(params, ctx)
}

if (isClient) {
const fn = queryFn as unknown as RpcClient
return fn(params, {fromInvoke: true}) as ReturnType<T>
Expand Down
1 change: 1 addition & 0 deletions packages/blitz-rpc/src/index-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import chalk from "chalk"

// TODO - optimize end user server bundles by not exporting all client stuff here
export * from "./index-browser"
export {RpcServerPlugin} from "./server/plugin"

export * from "./server/resolvers/resolver"

Expand Down
11 changes: 11 additions & 0 deletions packages/blitz-rpc/src/server/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {createServerPlugin} from "blitz"
import {invoke} from "../client/invoke"

export const RpcServerPlugin = createServerPlugin(() => {
return {
requestMiddlewares: [],
exports: () => ({
invoke,
}),
}
})

0 comments on commit 37aeaa7

Please sign in to comment.