-
-
Notifications
You must be signed in to change notification settings - Fork 249
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
255 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
"use client"; | ||
|
||
import * as css from "@/app/css"; | ||
|
||
import { useEffect, useState } from "react"; | ||
import { SessionData, defaultSession } from "./lib"; | ||
|
||
export function Form() { | ||
const [session, setSession] = useState<SessionData>(defaultSession); | ||
const [isLoading, setIsLoading] = useState(true); | ||
|
||
useEffect(() => { | ||
fetch("/app-router-magic-links/session") | ||
.then((res) => res.json()) | ||
.then((session) => { | ||
setSession(session); | ||
setIsLoading(false); | ||
}); | ||
}, []); | ||
|
||
if (isLoading) { | ||
return <p className="text-lg">Loading...</p>; | ||
} | ||
|
||
if (session.isLoggedIn) { | ||
return ( | ||
<> | ||
<p className="text-lg"> | ||
Logged in user: <strong>{session.username}</strong> | ||
</p> | ||
<LogoutButton /> | ||
</> | ||
); | ||
} | ||
|
||
return <LoginForm />; | ||
} | ||
|
||
function LoginForm() { | ||
return ( | ||
<form | ||
action="/app-router-magic-links/session" | ||
method="POST" | ||
className={css.form} | ||
> | ||
<label className="block text-lg"> | ||
<span className={css.label}>Username</span> | ||
<input | ||
type="text" | ||
name="username" | ||
className={css.input} | ||
placeholder="" | ||
defaultValue="Alison" | ||
required | ||
// for demo purposes, disabling autocomplete 1password here | ||
autoComplete="off" | ||
data-1p-ignore | ||
/> | ||
</label> | ||
<div> | ||
<input type="submit" value="Login" className={css.button} /> | ||
</div> | ||
</form> | ||
); | ||
} | ||
|
||
function LogoutButton() { | ||
return ( | ||
<p> | ||
<a | ||
href="/app-router-magic-links/session?action=logout" | ||
className={css.button} | ||
> | ||
Logout | ||
</a> | ||
</p> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { SessionOptions } from "iron-session"; | ||
|
||
export interface SessionData { | ||
username: string; | ||
isLoggedIn: boolean; | ||
} | ||
|
||
export const defaultSession: SessionData = { | ||
username: "", | ||
isLoggedIn: false, | ||
}; | ||
|
||
export const sessionOptions: SessionOptions = { | ||
password: "complex_password_at_least_32_characters_long", | ||
cookieName: "iron-examples-app-router-magic-links", | ||
cookieOptions: { | ||
secure: process.env.NODE_ENV === "production", | ||
}, | ||
}; | ||
|
||
export function sleep(ms: number) { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} |
22 changes: 22 additions & 0 deletions
22
examples/next/src/app/app-router-magic-links/magic-login/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { getIronSession, unsealData } from "iron-session"; | ||
import { cookies } from "next/headers"; | ||
import { NextRequest } from "next/server"; | ||
import { SessionData, sessionOptions } from "../lib"; | ||
|
||
export async function GET(request: NextRequest) { | ||
const seal = new URL(request.url).searchParams.get("seal") as string; | ||
const { username } = await unsealData<{ username: string }>(seal, { | ||
password: sessionOptions.password, | ||
}); | ||
const session = await getIronSession<SessionData>(cookies(), sessionOptions); | ||
session.isLoggedIn = true; | ||
session.username = username ?? "No username"; | ||
await session.save(); | ||
|
||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303 | ||
// not using redirect() yet: https://github.com/vercel/next.js/issues/51592#issuecomment-1810212676 | ||
return Response.redirect( | ||
`${request.nextUrl.origin}/app-router-magic-links`, | ||
303, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import * as css from "@/app/css"; | ||
import Link from "next/link"; | ||
|
||
import { Metadata } from "next"; | ||
import { GetTheCode } from "../../get-the-code"; | ||
import { Title } from "../title"; | ||
import { Form } from "./form"; | ||
|
||
export const metadata: Metadata = { | ||
title: "🛠 iron-session examples: Magic links", | ||
}; | ||
|
||
export default function AppRouterRedirect() { | ||
return ( | ||
<main className="p-10 space-y-5"> | ||
<Title subtitle="+ client components, route handlers, redirects, and fetch" /> | ||
|
||
<p className="italic max-w-xl"> | ||
<u>How to test</u>: Login and refresh the page to see iron-session in | ||
action. | ||
</p> | ||
|
||
<div className="grid grid-cols-1 gap-4 p-10 border border-slate-500 rounded-md max-w-xl"> | ||
<Form /> | ||
</div> | ||
|
||
<GetTheCode path="app/app-router-magic-links" /> | ||
<HowItWorks /> | ||
|
||
<p> | ||
<Link href="/" className={css.link}> | ||
← All examples | ||
</Link> | ||
</p> | ||
</main> | ||
); | ||
} | ||
|
||
function HowItWorks() { | ||
return ( | ||
<details className="max-w-2xl space-y-4"> | ||
<summary className="cursor-pointer">How it works</summary> | ||
|
||
<ol className="list-decimal list-inside"> | ||
<li> | ||
The form is submitted to /app-router-magic-links/session (API route) | ||
via a POST call (non-fetch). The API route generates a sealed token | ||
and returns the magic link to client so it can be either sent or used | ||
right away. When the magic link is visited it sets the session data | ||
and redirects back to /app-router-magic-links (this page) | ||
</li> | ||
<li> | ||
The page gets the session data via a fetch call to | ||
/app-router-magic-links/session (API route). The API route either | ||
return the session data (logged in) or a default session (not logged | ||
in). | ||
</li> | ||
<li> | ||
The logout is a regular link navigating to | ||
/app-router-magic-links/logout which destroy the session and redirects | ||
back to /app-router-magic-links (this page). | ||
</li> | ||
</ol> | ||
|
||
<p> | ||
<strong>Pros</strong>: Simple. | ||
</p> | ||
<p> | ||
<strong>Cons</strong>: Dangerous if not used properly. Without any | ||
invalidations or blacklists, the magic link can be used multiple times | ||
if compromised. | ||
</p> | ||
</details> | ||
); | ||
} |
49 changes: 49 additions & 0 deletions
49
examples/next/src/app/app-router-magic-links/session/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { getIronSession, sealData } from "iron-session"; | ||
import { cookies } from "next/headers"; | ||
import { redirect } from "next/navigation"; | ||
import { NextRequest } from "next/server"; | ||
import { SessionData, defaultSession, sessionOptions, sleep } from "../lib"; | ||
|
||
// /app-router-magic-links/session | ||
export async function POST(request: NextRequest) { | ||
const formData = await request.formData(); | ||
const username = formData.get("username") as string; | ||
const fifteenMinutesInSeconds = 15 * 60; | ||
|
||
const seal = await sealData( | ||
{ username }, | ||
{ | ||
password: "complex_password_at_least_32_characters_long", | ||
ttl: fifteenMinutesInSeconds, | ||
}, | ||
); | ||
|
||
return Response.json({ | ||
ok: true, | ||
// Ideally this would be an email or text message with a link to the magic link route | ||
magic_link: `${process.env.PUBLIC_URL}/app-router-magic-links/magic-login?seal=${seal}`, | ||
}); | ||
} | ||
|
||
// /app-router-magic-links/session | ||
// /app-router-magic-links/session?action=logout | ||
export async function GET(request: NextRequest) { | ||
const session = await getIronSession<SessionData>(cookies(), sessionOptions); | ||
|
||
console.log(new URL(request.url).searchParams); | ||
const action = new URL(request.url).searchParams.get("action"); | ||
// /app-router-magic-links/session?action=logout | ||
if (action === "logout") { | ||
session.destroy(); | ||
return redirect("/app-router-magic-links"); | ||
} | ||
|
||
// simulate looking up the user in db | ||
await sleep(250); | ||
|
||
if (session.isLoggedIn !== true) { | ||
return Response.json(defaultSession); | ||
} | ||
|
||
return Response.json(session); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters