Skip to content

Commit

Permalink
feat(next): add server actions support (#562)
Browse files Browse the repository at this point in the history
* feat(next): add server actions support

* refactor: apply suggestions from code review

---------

Co-authored-by: Gao Sun <gao@silverhand.io>
  • Loading branch information
wangsijie and gao-sun authored Oct 4, 2023
1 parent 41dc99b commit 5cc1342
Show file tree
Hide file tree
Showing 26 changed files with 1,514 additions and 166 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-suits-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@logto/next": minor
---

Add Next.js Server Actions support
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ cache
*.pem
.history
.vercel
.next
2 changes: 1 addition & 1 deletion packages/next-app-dir-sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@logto/next": "workspace:^2.1.0",
"next": "^13.3.1",
"next": "^13.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"server-only": "^0.0.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/next-sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@logto/next": "workspace:^2.1.0",
"next": "^13.3.1",
"next": "^13.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"swr": "^2.0.0"
Expand Down
3 changes: 3 additions & 0 deletions packages/next-server-actions-sample/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
21 changes: 21 additions & 0 deletions packages/next-server-actions-sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Next Sample (server actions)

This is a sample project for Logto's Next.js SDK (server actions).

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flogto-io%2Fjs%2Ftree%2Fmaster%2Fpackages%2Fnext-sample&env=APP_ID,APP_SECRET,ENDPOINT,BASE_URL,COOKIE_SECRET,RESOURCES,SCOPES&envDescription=Configuration%20needed%20to%20init%20Logto%20client&envLink=https%3A%2F%2Fgithub.com%2Flogto-io%2Fjs%2Ftree%2Fmaster%2Fpackages%2Fnext-server-actions-sample%2FREADME.md&project-name=logto-js&repository-name=logto-js)

## Configuration

You can configure the sample project by modifying the `libraries/config.js` file, or by setting the following environment variables:

| key | description | example |
| ------------- | ------------------------------------------------------- | ------------------------------------------------ |
| APP_ID | The app ID of your application | `my-app` |
| APP_SECRET | The app secret of your application | `my-secret` |
| ENDPOINT | The endpoint of your Logto server | `http://localhost:3001` |
| BASE_URL | The base URL of this application | `http://localhost:3000` |
| COOKIE_SECRET | The secret for cookie encryption | `my-cookie-secret` |
| RESOURCES | Optional, the API resource identifier, split with comma | `http://localhost:3003/,http://localhost:3004/]` |
| SCOPES | Optional, the scopes to grant, split with comma | `read:users,write:users` |

Learn more about resource and scopes in the [Logto RBAC Documentation](https://docs.logto.io/docs/recipes/rbac/protect-resource#configure-client-sdk).
21 changes: 21 additions & 0 deletions packages/next-server-actions-sample/app/callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import { useRouter } from "next/navigation";
import { handleSignIn } from "../../libraries/logto";
import { useEffect } from "react";

type Props = {
searchParams: Record<string, string>;
};

export default function Callback({ searchParams }: Props) {
const router = useRouter();

useEffect(() => {
handleSignIn(searchParams).then(() => {
router.push("/");
});
}, [router, searchParams]);

return <div>Signing in...</div>;
}
19 changes: 19 additions & 0 deletions packages/next-server-actions-sample/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Hello Logto",
description:
"Example project for integrating Logto with Next.js Server Actions",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
34 changes: 34 additions & 0 deletions packages/next-server-actions-sample/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getLogtoContext } from "../libraries/logto";
import SignIn from "./sign-in";
import SignOut from "./sign-out";

export default async function Home() {
const { isAuthenticated, claims } = await getLogtoContext();
return (
<main>
<h1>Hello Logto.</h1>
<div>{isAuthenticated ? <SignOut /> : <SignIn />}</div>
{claims && (
<div>
<h2>Claims:</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{Object.entries(claims).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
);
}
18 changes: 18 additions & 0 deletions packages/next-server-actions-sample/app/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { useRouter } from "next/navigation";
import { signIn } from "../libraries/logto";

const SignIn = () => {
const router = useRouter();

const handleClick = async () => {
const redirectUrl = await signIn();

router.push(redirectUrl);
};

return <button onClick={handleClick}>Sign In</button>;
};

export default SignIn;
18 changes: 18 additions & 0 deletions packages/next-server-actions-sample/app/sign-out.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { useRouter } from "next/navigation";
import { signOut } from "../libraries/logto";

const SignOut = () => {
const router = useRouter();

const handleClick = async () => {
const redirectUrl = await signOut();

router.push(redirectUrl);
};

return <button onClick={handleClick}>Sign Out</button>;
};

export default SignOut;
74 changes: 74 additions & 0 deletions packages/next-server-actions-sample/libraries/logto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use server";

import LogtoClient from "@logto/next/server-actions";
import { cookies } from "next/headers";

const config = {
appId: process.env.APP_ID ?? "<app-id>",
appSecret: process.env.APP_SECRET ?? "<app-secret>",
endpoint: process.env.ENDPOINT ?? "http://localhost:3001",
baseUrl: process.env.BASE_URL ?? "http://localhost:3000",
cookieSecret:
process.env.COOKIE_SECRET ?? "complex_password_at_least_32_characters_long",
cookieSecure: process.env.NODE_ENV === "production",
// Optional fields for RBAC
resources: process.env.RESOURCES?.split(","),
scopes: process.env.SCOPES?.split(","),
};

const logtoClient = new LogtoClient(config);

const cookieName = `logto:${config.appId}`;

const setCookies = (value?: string) => {
if (value === undefined) {
return;
}

cookies().set(cookieName, value, {
maxAge: 14 * 3600 * 24,
secure: config.cookieSecure,
});
};

const getCookie = () => {
return cookies().get(cookieName)?.value ?? "";
};

export const signIn = async () => {
const { url, newCookie } = await logtoClient.handleSignIn(
getCookie(),
`${config.baseUrl}/callback`
);

setCookies(newCookie);

return url;
};

export const handleSignIn = async (searchParams: Record<string, string>) => {
// Convert searchParams object into a query string.
const search = new URLSearchParams(searchParams).toString();

const newCookie = await logtoClient.handleSignInCallback(
getCookie(),
`${config.baseUrl}/callback?${search}`
);

setCookies(newCookie);
};

export const signOut = async () => {
const url = await logtoClient.handleSignOut(
getCookie(),
`${config.baseUrl}/callback`
);

setCookies('');

return url;
};

export const getLogtoContext = async () => {
return await logtoClient.getLogtoContext(getCookie());
};
5 changes: 5 additions & 0 deletions packages/next-server-actions-sample/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
8 changes: 8 additions & 0 deletions packages/next-server-actions-sample/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
};

module.exports = nextConfig;
36 changes: 36 additions & 0 deletions packages/next-server-actions-sample/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "next-server-actions-sample",
"version": "2.1.0",
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@logto/next": "workspace:^",
"next": "^13.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@silverhand/ts-config": "^4.0.0",
"@silverhand/ts-config-react": "^4.0.0",
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"eslint": "^8.50.0",
"eslint-config-next": "latest",
"lint-staged": "^14.0.0",
"postcss": "^8.4.6",
"postcss-modules": "^6.0.0",
"prettier": "^3.0.0",
"stylelint": "^15.0.0",
"typescript": "latest"
},
"stylelint": {
"extends": "@silverhand/eslint-config-react/.stylelintrc"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
}
22 changes: 22 additions & 0 deletions packages/next-server-actions-sample/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": "@silverhand/ts-config-react/tsconfig.base",
"compilerOptions": {
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"allowJs": true // added by next cli
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
1 change: 1 addition & 0 deletions packages/next/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import baseConfig from '../../jest.config.js';
/** @type {import('jest').Config} */
const config = {
...baseConfig,
roots: ['<rootDir>/src', '<rootDir>/server-actions'],
setupFilesAfterEnv: ['jest-matcher-specific-error'],
};

Expand Down
10 changes: 9 additions & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
],
"edge": [
"./lib/edge/index.d.ts"
],
"server-actions": [
"./lib/server-actions/index.d.ts"
]
}
},
Expand All @@ -25,6 +28,11 @@
"require": "./lib/edge/index.cjs",
"import": "./lib/edge/index.js",
"types": "./lib/edge/index.d.ts"
},
"./server-actions": {
"require": "./lib/server-actions/index.cjs",
"import": "./lib/server-actions/index.js",
"types": "./lib/server-actions/index.d.ts"
}
},
"files": [
Expand Down Expand Up @@ -63,7 +71,7 @@
"jest-location-mock": "^2.0.0",
"jest-matcher-specific-error": "^1.0.0",
"lint-staged": "^14.0.0",
"next": "^13.0.4",
"next": "^13.5.3",
"next-test-api-route-handler": "^3.1.6",
"prettier": "^3.0.0",
"react": "^18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/next/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import baseConfigs from '../../rollup.config.js';

const configs = {
...baseConfigs,
input: ['src/index.ts', 'edge/index.ts'],
input: ['src/index.ts', 'edge/index.ts', 'server-actions/index.ts'],
};

export default configs;
Loading

0 comments on commit 5cc1342

Please sign in to comment.