Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add @ory/nextjs package #303

Merged
merged 1 commit into from
Dec 5, 2024
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
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"

Check warning on line 4 in packages/nextjs/src/app/client.ts

View check run for this annotation

Codecov / codecov/patch

packages/nextjs/src/app/client.ts#L4

Added line #L4 was not covered by tests

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

Check warning on line 6 in packages/nextjs/src/app/client.ts

View check run for this annotation

Codecov / codecov/patch

packages/nextjs/src/app/client.ts#L6

Added line #L6 was not covered by tests

export const serverSideFrontendClient = new FrontendApi(

Check warning on line 8 in packages/nextjs/src/app/client.ts

View check run for this annotation

Codecov / codecov/patch

packages/nextjs/src/app/client.ts#L8

Added line #L8 was not covered by tests
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
Loading