Link to iron-session example (#62808)

## What?

The current example is outdated so I've removed it and linked to the
package's own example.

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->


Closes NEXT-2680
This commit is contained in:
Tim Neutkens 2024-03-04 14:46:28 +01:00 committed by GitHub
parent 04bda302e4
commit 2f062d61cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 2 additions and 791 deletions

View file

@ -1,6 +0,0 @@
# ⚠️ The SECRET_COOKIE_PASSWORD should never be inside your repository directly, it's here only to ease
# the example deployment
# For local development, you should store it inside a `.env.local` gitignored file
# See https://nextjs.org/docs/basic-features/environment-variables#loading-environment-variables
SECRET_COOKIE_PASSWORD=2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8

View file

@ -1,6 +0,0 @@
# ⚠️ The SECRET_COOKIE_PASSWORD should never be inside your repository directly, it's here only to ease
# the example deployment
# For production you should use https://vercel.com/blog/environment-variables-ui if you're hosted on Vercel or
# any other secret environment variable mean
SECRET_COOKIE_PASSWORD=2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8

View file

@ -1,46 +1,3 @@
# Example application using [`iron-session`](https://github.com/vvo/iron-session) # Next.js + iron-session
<p align="center"><b>👀 Online demo at <a href="https://iron-session-example.vercel.app/">https://iron-session-example.vercel.app</a></b></p> You can find the [iron-session example here](https://github.com/vvo/iron-session/tree/main/examples/next).
---
This example creates an authentication system that uses a **signed and encrypted cookie to store session data**. It relies on [`iron-session`](https://github.com/vvo/iron-session).
It uses current best practices for authentication in the Next.js ecosystem and replicates parts of how the Vercel dashboard is built.
**Features of the example:**
- [API Routes](https://nextjs.org/docs/api-routes/dynamic-api-routes) and [getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) examples.
- The logged in status is synchronized between browser windows/tabs using **`useUser`** hook and the [`swr`](https://swr.vercel.app/).
- The layout is based on the user's logged-in/out status.
- The session data is signed and encrypted in a cookie (this is done automatically by `iron-session`).
[`iron-session`](https://github.com/vvo/iron-session) also provides:
- An Express middleware, which can be used in any Node.js HTTP framework.
- Multiple encryption keys (passwords) to allow for seamless updates or just password rotation.
- Full TypeScript support, including session data.
## Deploy your own
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-iron-session)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-iron-session&project-name=with-iron-session&repository-name=with-iron-session)
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
```bash
npx create-next-app --example with-iron-session with-iron-session-app
```
```bash
yarn create next-app --example with-iron-session with-iron-session-app
```
```bash
pnpm create next-app --example with-iron-session with-iron-session-app
```
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).

View file

@ -1,43 +0,0 @@
import { FormEvent } from "react";
export default function Form({
errorMessage,
onSubmit,
}: {
errorMessage: string;
onSubmit: (e: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form onSubmit={onSubmit}>
<label>
<span>Type your GitHub username</span>
<input type="text" name="username" required />
</label>
<button type="submit">Login</button>
{errorMessage && <p className="error">{errorMessage}</p>}
<style jsx>{`
form,
label {
display: flex;
flex-flow: column;
}
label > span {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
color: brown;
margin: 1rem 0 0;
}
`}</style>
</form>
);
}

View file

@ -1,121 +0,0 @@
import Link from "next/link";
import useUser from "lib/useUser";
import { useRouter } from "next/router";
import Image from "next/image";
import fetchJson from "lib/fetchJson";
export default function Header() {
const { user, mutateUser } = useUser();
const router = useRouter();
return (
<header>
<nav>
<ul>
<li>
<Link href="/" legacyBehavior>
<a>Home</a>
</Link>
</li>
{user?.isLoggedIn === false && (
<li>
<Link href="/login" legacyBehavior>
<a>Login</a>
</Link>
</li>
)}
{user?.isLoggedIn === true && (
<>
<li>
<Link href="/profile-sg" legacyBehavior>
<a>
<span
style={{
marginRight: ".3em",
verticalAlign: "middle",
borderRadius: "100%",
overflow: "hidden",
}}
>
<Image
src={user.avatarUrl}
width={32}
height={32}
alt=""
/>
</span>
Profile (Static Generation, recommended)
</a>
</Link>
</li>
<li>
<Link href="/profile-ssr" legacyBehavior>
<a>Profile (Server-side Rendering)</a>
</Link>
</li>
<li>
<a
href="/api/logout"
onClick={async (e) => {
e.preventDefault();
mutateUser(
await fetchJson("/api/logout", { method: "POST" }),
false,
);
router.push("/login");
}}
>
Logout
</a>
</li>
</>
)}
<li>
<a href="https://github.com/vvo/iron-session">
<Image
src="/GitHub-Mark-Light-32px.png"
width="32"
height="32"
alt=""
/>
</a>
</li>
</ul>
</nav>
<style jsx>{`
ul {
display: flex;
list-style: none;
margin-left: 0;
padding-left: 0;
}
li {
margin-right: 1rem;
display: flex;
}
li:first-child {
margin-left: auto;
}
a {
color: #fff;
text-decoration: none;
display: flex;
align-items: center;
}
a img {
margin-right: 1em;
}
header {
padding: 0.2rem;
color: #fff;
background-color: #333;
}
`}</style>
</header>
);
}

View file

@ -1,39 +0,0 @@
import Head from "next/head";
import Header from "components/Header";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Head>
<title>With Iron Session</title>
</Head>
<style jsx global>{`
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, Noto Sans, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.container {
max-width: 65rem;
margin: 1.5rem auto;
padding-left: 1rem;
padding-right: 1rem;
}
`}</style>
<Header />
<main>
<div className="container">{children}</div>
</main>
</>
);
}

View file

@ -1,52 +0,0 @@
export class FetchError extends Error {
response: Response;
data: {
message: string;
};
constructor({
message,
response,
data,
}: {
message: string;
response: Response;
data: {
message: string;
};
}) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(message);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, FetchError);
}
this.name = "FetchError";
this.response = response;
this.data = data ?? { message: message };
}
}
export default async function fetchJson<JSON = unknown>(
input: RequestInfo,
init?: RequestInit,
): Promise<JSON> {
const response = await fetch(input, init);
// if the server replies, there's always some data in json
// if there's a network error, it will throw at the previous line
const data = await response.json();
// response.ok is true when res.status is 2xx
// https://developer.mozilla.org/docs/Web/API/Response/ok
if (response.ok) {
return data;
}
throw new FetchError({
message: response.statusText,
response,
data,
});
}

View file

@ -1,19 +0,0 @@
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import type { IronSessionOptions } from "iron-session";
import type { User } from "pages/api/user";
export const sessionOptions: IronSessionOptions = {
password: process.env.SECRET_COOKIE_PASSWORD as string,
cookieName: "iron-session/examples/next.js",
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
};
// This is where we specify the typings of req.session.*
declare module "iron-session" {
interface IronSessionData {
user?: User;
}
}

View file

@ -1,12 +0,0 @@
import useSWR from "swr";
import type { User } from "pages/api/user";
import type { Events } from "pages/api/events";
export default function useEvents(user: User | undefined) {
// We do a request to /api/events only if the user is logged in
const { data: events } = useSWR<Events>(
user?.isLoggedIn ? `/api/events` : null,
);
return { events };
}

View file

@ -1,28 +0,0 @@
import { useEffect } from "react";
import Router from "next/router";
import useSWR from "swr";
import { User } from "pages/api/user";
export default function useUser({
redirectTo = "",
redirectIfFound = false,
} = {}) {
const { data: user, mutate: mutateUser } = useSWR<User>("/api/user");
useEffect(() => {
// if no redirect needed, just return (example: already on /dashboard)
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
if (!redirectTo || !user) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !user?.isLoggedIn) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && user?.isLoggedIn)
) {
Router.push(redirectTo);
}
}, [user, redirectIfFound, redirectTo]);
return { user, mutateUser };
}

View file

@ -1,5 +0,0 @@
/// <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.

View file

@ -1,13 +0,0 @@
/** @type {import('next').NextConfig} */
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
port: "",
pathname: "/my-account/**",
},
],
},
};

View file

@ -1,21 +0,0 @@
{
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start"
},
"dependencies": {
"iron-session": "latest",
"next": "latest",
"octokit": "^1.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"swr": "^2.0.0"
},
"devDependencies": {
"@octokit/types": "^6.34.0",
"@types/react": "^17.0.34",
"typescript": "^4.4.4"
}
}

View file

@ -1,20 +0,0 @@
import { AppProps } from "next/app";
import { SWRConfig } from "swr";
import fetchJson from "lib/fetchJson";
function MyApp({ Component, pageProps }: AppProps) {
return (
<SWRConfig
value={{
fetcher: fetchJson,
onError: (err) => {
console.error(err);
},
}}
>
<Component {...pageProps} />
</SWRConfig>
);
}
export default MyApp;

View file

@ -1,33 +0,0 @@
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "lib/session";
import { Octokit } from "octokit";
import type { Endpoints } from "@octokit/types";
import { NextApiRequest, NextApiResponse } from "next";
export type Events =
Endpoints["GET /users/{username}/events"]["response"]["data"];
const octokit = new Octokit();
async function eventsRoute(req: NextApiRequest, res: NextApiResponse<Events>) {
const user = req.session.user;
if (!user || user.isLoggedIn === false) {
res.status(401).end();
return;
}
try {
const { data: events } =
await octokit.rest.activity.listPublicEventsForUser({
username: user.login,
});
res.json(events);
} catch (error) {
res.status(200).json([]);
}
}
export default withIronSessionApiRoute(eventsRoute, sessionOptions);

View file

@ -1,26 +0,0 @@
import type { User } from "./user";
import { Octokit } from "octokit";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "lib/session";
import { NextApiRequest, NextApiResponse } from "next";
const octokit = new Octokit();
async function loginRoute(req: NextApiRequest, res: NextApiResponse) {
const { username } = await req.body;
try {
const {
data: { login, avatar_url },
} = await octokit.rest.users.getByUsername({ username });
const user = { isLoggedIn: true, login, avatarUrl: avatar_url } as User;
req.session.user = user;
await req.session.save();
res.json(user);
} catch (error) {
res.status(500).json({ message: (error as Error).message });
}
}
export default withIronSessionApiRoute(loginRoute, sessionOptions);

View file

@ -1,11 +0,0 @@
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "lib/session";
import { NextApiRequest, NextApiResponse } from "next";
import type { User } from "pages/api/user";
function logoutRoute(req: NextApiRequest, res: NextApiResponse<User>) {
req.session.destroy();
res.json({ isLoggedIn: false, login: "", avatarUrl: "" });
}
export default withIronSessionApiRoute(logoutRoute, sessionOptions);

View file

@ -1,28 +0,0 @@
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "lib/session";
import { NextApiRequest, NextApiResponse } from "next";
export type User = {
isLoggedIn: boolean;
login: string;
avatarUrl: string;
};
async function userRoute(req: NextApiRequest, res: NextApiResponse<User>) {
if (req.session.user) {
// in a real world application you might read the user id from the session and then do a database request
// to get more information on the user if needed
res.json({
...req.session.user,
isLoggedIn: true,
});
} else {
res.json({
isLoggedIn: false,
login: "",
avatarUrl: "",
});
}
}
export default withIronSessionApiRoute(userRoute, sessionOptions);

View file

@ -1,59 +0,0 @@
import Layout from "components/Layout";
import Image from "next/image";
export default function Home() {
return (
<Layout>
<h1>
<span style={{ marginRight: ".3em", verticalAlign: "middle" }}>
<Image src="/GitHub-Mark-32px.png" width="32" height="32" alt="" />
</span>
<a href="https://github.com/vvo/iron-session">iron-session</a> -
Authentication example
</h1>
<p>
This example creates an authentication system that uses a{" "}
<b>signed and encrypted cookie to store session data</b>.
</p>
<p>
It uses current best practices as for authentication in the Next.js
ecosystem:
<br />
1. <b>no `getInitialProps`</b> to ensure every page is static
<br />
2. <b>`useUser` hook</b> together with `
<a href="https://swr.vercel.app/">swr`</a> for data fetching
</p>
<h2>Features</h2>
<ul>
<li>Logged in status synchronized between browser windows/tabs</li>
<li>Layout based on logged in status</li>
<li>All pages are static</li>
<li>Session data is signed and encrypted in a cookie</li>
</ul>
<h2>Steps to test the functionality:</h2>
<ol>
<li>Click login and enter your GitHub username.</li>
<li>
Click home and click profile again, notice how your session is being
used through a token stored in a cookie.
</li>
<li>
Click logout and try to go to profile again. You&apos;ll get
redirected to the `/login` route.
</li>
</ol>
<style jsx>{`
li {
margin-bottom: 0.5rem;
}
`}</style>
</Layout>
);
}

View file

@ -1,57 +0,0 @@
import React, { useState } from "react";
import useUser from "lib/useUser";
import Layout from "components/Layout";
import Form from "components/Form";
import fetchJson, { FetchError } from "lib/fetchJson";
export default function Login() {
// here we just check if user is already logged in and redirect to profile
const { mutateUser } = useUser({
redirectTo: "/profile-sg",
redirectIfFound: true,
});
const [errorMsg, setErrorMsg] = useState("");
return (
<Layout>
<div className="login">
<Form
errorMessage={errorMsg}
onSubmit={async function handleSubmit(event) {
event.preventDefault();
const body = {
username: event.currentTarget.username.value,
};
try {
mutateUser(
await fetchJson("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}),
);
} catch (error) {
if (error instanceof FetchError) {
setErrorMsg(error.data.message);
} else {
console.error("An unexpected error happened:", error);
}
}
}}
/>
</div>
<style jsx>{`
.login {
max-width: 21rem;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
`}</style>
</Layout>
);
}

View file

@ -1,50 +0,0 @@
import React from "react";
import Layout from "components/Layout";
import useUser from "lib/useUser";
import useEvents from "lib/useEvents";
// Make sure to check https://nextjs.org/docs/basic-features/layouts for more info on how to use layouts
export default function SgProfile() {
const { user } = useUser({
redirectTo: "/login",
});
const { events } = useEvents(user);
return (
<Layout>
<h1>Your GitHub profile</h1>
<h2>
This page uses{" "}
<a href="https://nextjs.org/docs/basic-features/pages#static-generation-recommended">
Static Generation (SG)
</a>{" "}
and the <a href="/api/user">/api/user</a> route (using{" "}
<a href="https://github.com/vercel/swr">vercel/SWR</a>)
</h2>
{user && (
<>
<p style={{ fontStyle: "italic" }}>
Public data, from{" "}
<a href={`https://github.com/${user.login}`}>
https://github.com/{user.login}
</a>
, reduced to `login` and `avatar_url`.
</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)}
{events !== undefined && (
<p>
Number of GitHub events for user: <b>{events.length}</b>.{" "}
{events.length > 0 && (
<>
Last event type: <b>{events[0].type}</b>
</>
)}
</p>
)}
</Layout>
);
}

View file

@ -1,63 +0,0 @@
import React from "react";
import Layout from "components/Layout";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "lib/session";
import { User } from "pages/api/user";
import { InferGetServerSidePropsType } from "next";
export default function SsrProfile({
user,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<Layout>
<h1>Your GitHub profile</h1>
<h2>
This page uses{" "}
<a href="https://nextjs.org/docs/basic-features/pages#server-side-rendering">
Server-side Rendering (SSR)
</a>{" "}
and{" "}
<a href="https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props">
getServerSideProps
</a>
</h2>
{user?.isLoggedIn && (
<>
<p style={{ fontStyle: "italic" }}>
Public data, from{" "}
<a href={`https://github.com/${user.login}`}>
https://github.com/{user.login}
</a>
, reduced to `login` and `avatar_url`.
</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)}
</Layout>
);
}
export const getServerSideProps = withIronSessionSsr(async function ({
req,
res,
}) {
const user = req.session.user;
if (user === undefined) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return {
props: {
user: { isLoggedIn: false, login: "", avatarUrl: "" } as User,
},
};
}
return {
props: { user: req.session.user },
};
},
sessionOptions);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js"],
"exclude": ["node_modules"]
}

View file

@ -1,13 +0,0 @@
{
"env": {
"SECRET_COOKIE_PASSWORD": "2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8"
},
"build": {
"env": {
"SECRET_COOKIE_PASSWORD": "2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8"
}
},
"github": {
"silent": true
}
}