Skip to content

Commit

Permalink
feat: add @ory/nextjs package (#303)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-jonas authored Dec 5, 2024
1 parent 4c0db56 commit 64c9b50
Show file tree
Hide file tree
Showing 47 changed files with 3,239 additions and 165 deletions.
13 changes: 13 additions & 0 deletions .github/codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
comment:
layout: "condensed_header, diff, flags, components"

component_management:
individual_components:
- component_id: elements-react # this is an identifier that should not be changed
name: "@ory/elements-react" # this is a display name, and can be changed freely
paths:
- packages/elements-react
- component_id: nextjs # this is an identifier that should not be changed
name: "@ory/nextjs" # this is a display name, and can be changed freely
paths:
- packages/nextjs
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ jobs:
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
codecov_yml_path: .github/codecov.yml
934 changes: 769 additions & 165 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions packages/nextjs/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
// "extends": ["../../.eslintrc.base.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": "error"
}
}
]
}
17 changes: 17 additions & 0 deletions packages/nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# nextjs

This library was generated with [Nx](https://nx.dev).

## Building

Run `nx build @ory/nextjs` to build the library.

## Developing

Run `nx dev @ory/nextjs` to watch the source code for changes and continuously
build the library.

## Running unit tests

Run `nx test @ory/nextjs` to execute the unit tests via
[Jest](https://jestjs.io).
15 changes: 15 additions & 0 deletions packages/nextjs/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

/* eslint-disable */
export default {
displayName: "nextjs",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
collectCoverageFrom: ["src/**/*.ts", "src/**/*.js"],
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/packages/nextjs",
}
73 changes: 73 additions & 0 deletions packages/nextjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "@ory/nextjs",
"version": "0.0.1",
"type": "commonjs",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"private": false,
"dependencies": {
"@ory/client-fetch": "^1.15.6",
"cookie": "^1.0.1",
"psl": "^1.10.0",
"set-cookie-parser": "^2.7.1"
},
"devDependencies": {
"@types/cookie": "^0.6.0",
"@types/psl": "^1.1.3",
"@types/set-cookie-parser": "^2.4.10",
"babel-jest": "^29.7.0",
"jest-esm-transformer": "^1.0.0",
"tsup": "8.3.0"
},
"keywords": [
"ory",
"auth",
"react",
"passwordless",
"login",
"user management",
"permissions",
"authentication",
"nextjs",
"vercel",
"app router",
"pages router"
],
"peerDependencies": {
"next": ">=13.1.0",
"react": ">=16.0.0"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./middleware": {
"types": "./dist/middleware/index.d.ts",
"import": "./dist/middleware/index.mjs",
"require": "./dist/middleware/index.js"
},
"./app": {
"types": "./dist/app/index.d.ts",
"import": "./dist/app/index.mjs",
"require": "./dist/app/index.js"
},
"./pages": {
"types": "./dist/pages/index.d.ts",
"import": "./dist/pages/index.mjs",
"require": "./dist/pages/index.js"
}
},
"typesVersions": {
"*": {
"index": [
"./dist/index.d.ts"
],
"middleware": [
"./dist/middleware/index.d.ts"
]
}
}
}
30 changes: 30 additions & 0 deletions packages/nextjs/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@ory/nextjs",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/nextjs/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"command": "tsup --clean --dts",
"options": {
"cwd": "packages/nextjs"
}
},
"dev": {
"command": "tsup --watch --dts",
"options": {
"cwd": "packages/nextjs"
}
},
"test": {
"executor": "@nx/jest:jest",
"dependsOn": ["build"],
"options": {
"jestConfig": "packages/nextjs/jest.config.ts",
"coverage": true,
"coverageReporters": ["text", "cobertura"]
}
}
}
}
15 changes: 15 additions & 0 deletions packages/nextjs/src/app/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { Configuration, FrontendApi } from "@ory/client-fetch"

import { orySdkUrl } from "../utils/sdk"

export const serverSideFrontendClient = new FrontendApi(
new Configuration({
headers: {
Accept: "application/json",
},
basePath: orySdkUrl(),
}),
)
124 changes: 124 additions & 0 deletions packages/nextjs/src/app/flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { FlowType, handleFlowError } from "@ory/client-fetch"
import {
getLoginFlow,
getRecoveryFlow,
getRegistrationFlow,
getVerificationFlow,
} from "."
import { redirect } from "next/navigation"
import { getPublicUrl, toFlowParams } from "./utils"
import { serverSideFrontendClient } from "./client"

jest.mock("./utils", () => ({
getPublicUrl: jest.fn(),
toFlowParams: jest.fn().mockImplementation((params) => params),
}))

jest.mock("./client", () => ({
serverSideFrontendClient: {
getLoginFlowRaw: jest.fn(),
getRegistrationFlowRaw: jest.fn(),
getRecoveryFlowRaw: jest.fn(),
getVerificationFlowRaw: jest.fn(),
},
}))

jest.mock("next/navigation", () => ({
redirect: jest.fn(),
RedirectType: {
replace: "replace",
},
}))

jest.mock("@ory/client-fetch", () => {
const original = jest.requireActual("@ory/client-fetch")

return {
...original,
handleFlowError: jest.fn(),
}
})

beforeEach(() => {
;(getPublicUrl as jest.Mock).mockResolvedValue("https://example.com")
jest.clearAllMocks()
process.env["NEXT_PUBLIC_ORY_SDK_URL"] = "https://ory.sh/"
;(handleFlowError as jest.Mock).mockReturnValue(async () => {})
})

const testCases = [
{
fn: getLoginFlow,
flowType: FlowType.Login,
m: serverSideFrontendClient.getLoginFlowRaw,
},
{
fn: getRegistrationFlow,
flowType: FlowType.Registration,
m: serverSideFrontendClient.getRegistrationFlowRaw,
},
{
fn: getRecoveryFlow,
flowType: FlowType.Recovery,
m: serverSideFrontendClient.getRecoveryFlowRaw,
},
{
fn: getVerificationFlow,
flowType: FlowType.Verification,
m: serverSideFrontendClient.getVerificationFlowRaw,
},
]

for (const tc of testCases) {
describe(`flowtype=${tc.flowType}`, () => {
test("restarts flow if no id given", async () => {
const queryParams = {}
await tc.fn(queryParams)
expect(redirect).toHaveBeenCalledWith(
`https://example.com/self-service/${tc.flowType}/browser`,
"replace",
)
})

test("restarts flow if no id is given with query params", async () => {
const queryParams = {
refresh: "true",
}
await tc.fn(queryParams)
expect(redirect).toHaveBeenCalledWith(
`https://example.com/self-service/${tc.flowType}/browser?refresh=true`,
"replace",
)
})

test("fetches flow and rewrite json response", async () => {
const queryParams = {
flow: "1234",
}
;(tc.m as jest.Mock).mockResolvedValue({
value: jest.fn().mockResolvedValue({
foo: "https://ory.sh/a",
bar: "https://ory.sh/",
}),
} as any)
const result = await tc.fn(queryParams)
expect(result).toEqual({
foo: "https://example.com/a",
bar: "https://example.com/",
})
})

test("fetches flow and calls error handler on error", async () => {
const queryParams = {
flow: "1234",
}
;(tc.m as jest.Mock).mockRejectedValue(new Error("error"))
const result = await tc.fn(queryParams)
expect(result).toBeNull()
expect(handleFlowError).toHaveBeenCalled()
})
})
}
53 changes: 53 additions & 0 deletions packages/nextjs/src/app/flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { redirect, RedirectType } from "next/navigation"
import { FlowType, handleFlowError } from "@ory/client-fetch"

import { getPublicUrl, onRedirect } from "./utils"
import { QueryParams } from "../types"
import { guessPotentiallyProxiedOrySdkUrl } from "../utils/sdk"
import { onValidationError } from "../utils/utils"
import { rewriteJsonResponse } from "../utils/rewrite"
import * as runtime from "@ory/client-fetch/src/runtime"

export async function getFlow<T extends object>(
params: QueryParams,
fetchFlowRaw: () => Promise<runtime.ApiResponse<T>>,
flowType: FlowType,
): Promise<T | null | void> {
// Guess our own public url using Next.js helpers. We need the hostname, port, and protocol.
const knownProxiedUrl = await getPublicUrl()
const url = guessPotentiallyProxiedOrySdkUrl({
knownProxiedUrl,
})

const onRestartFlow = () => {
const redirectTo = new URL(
"/self-service/" + flowType.toString() + "/browser",
url,
)
redirectTo.search = new URLSearchParams(params).toString()
return redirect(redirectTo.toString(), RedirectType.replace)
}

if (!params["flow"]) {
onRestartFlow()
return
}

try {
const rawResponse = await fetchFlowRaw()
return await rawResponse
.value()
.then((v: T): T => rewriteJsonResponse(v, url))
} catch (error) {
const errorHandler = handleFlowError({
onValidationError,
onRestartFlow,
onRedirect: onRedirect,
})
await errorHandler(error)
return null
}
}
10 changes: 10 additions & 0 deletions packages/nextjs/src/app/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
"use server"

export { getLoginFlow } from "./login"
export { getRegistrationFlow } from "./registration"
export { getRecoveryFlow } from "./recovery"
export { getVerificationFlow } from "./verification"

export type { OryPageParams } from "./utils"
Loading

0 comments on commit 64c9b50

Please sign in to comment.