Skip to content

Commit

Permalink
add audiences
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity committed Oct 24, 2024
1 parent f127e99 commit 0d9a7a7
Show file tree
Hide file tree
Showing 16 changed files with 3,127 additions and 1,035 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/preview-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: preview-docs

on:
pull_request:

jobs:
run:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Fern
run: npm install -g fern-api

- name: Generate preview URL
id: generate-docs
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: |
OUTPUT=$(fern generate --docs --preview 2>&1) || true
echo "$OUTPUT"
URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()')
echo "Preview URL: $URL"
echo "🌿 Preview your docs: $URL" > preview_url.txt
- name: Comment URL in PR
uses: thollander/[email protected]
with:
filePath: preview_url.txt
21 changes: 21 additions & 0 deletions .github/workflows/publish-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: publish-docs

on:
push:
branches:
- main

jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Fern
run: npm install -g fern-api

- name: Publish Docs
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: fern generate --docs --log-level debug
7 changes: 7 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"plugins": ["prettier-plugin-tailwindcss"]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"npm.packageManager": "pnpm"
}
76 changes: 45 additions & 31 deletions app/api/login/route.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,84 @@
import { SignJWT } from "jose";
import { NextRequest, NextResponse } from "next/server";
import { SignJWT } from 'jose'
import { NextRequest, NextResponse } from 'next/server'

// Please reach out to the Fern Team if you have any questions about this setup.
// In this example, we show how you can write a lambda handler (or cloudflare worker) can be setup after the user logs in via your identity provider
// and mint a JWT (which we refer to as the `fern_token`) which will give users access to VIEW your docs.

// your domain
const JWT_ISSUER = "https://yourdomain.com"
const JWT_ISSUER = 'https://test-jwt-auth-smoky.vercel.app'

// this is the domain of your docs, (or, if subpathed, use the apex here: yourdomain.com);
const DOCS_ORIGIN = "https://docs.yourdomain.com";
const DOCS_ORIGIN = 'https://test-jwt-auth-smoky.docs.buildwithfern.com'

// this is path that you will redirect to in the docs instance, and let Fern set the fern_token. This is preferred.
// alternatively, you may opt to set the `Set-Cookie` header directly using `fern_token` (case sensitive) if on the same domain.
// if subpathed docs, be sure to include the subpath set by your proxy, i.e. `/docs/api/fern-docs/auth/jwt/callback`.
const JWT_CALLBACK_PATHNAME = "/api/fern-docs/auth/jwt/callback";
const JWT_CALLBACK_PATHNAME = '/api/fern-docs/auth/jwt/callback'

// JWT payload must include a `fern` key. All fields are optional:
interface FernUser {
apiKey?: string; // api key injection into the runnable API Playground
audience?: string[]; // ACLs -> this controls which part of the docs are visible to the user
apiKey?: string // api key injection into the runnable API Playground
audience?: string[] // ACLs -> this controls which part of the docs are visible to the user
}

// this is the symmetric secret key that will be provided by Fern:
function getJwtTokenSecret(): Uint8Array {
if (!process.env.JWT_TOKEN_SECRET) {
throw new Error("JWT_TOKEN_SECRET is not set");
throw new Error('JWT_TOKEN_SECRET is not set')
}
return new TextEncoder().encode(process.env.JWT_TOKEN_SECRET);
return new TextEncoder().encode(process.env.JWT_TOKEN_SECRET)
}

function signFernJWT(fern: FernUser): Promise<string> {
return new SignJWT({ fern })
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuedAt()
// be sure to set an appropriate expiration duration based on the level of sensitivity (i.e. api key) contained within the token, and the access that the token confers
.setExpirationTime("30d")
.setIssuer(JWT_ISSUER)
.sign(getJwtTokenSecret());
export function signFernJWT(fern: FernUser): Promise<string> {
return (
new SignJWT({ fern })
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
// be sure to set an appropriate expiration duration based on the level of sensitivity (i.e. api key) contained within the token, and the access that the token confers
.setExpirationTime('30d')
.setIssuer(JWT_ISSUER)
.sign(getJwtTokenSecret())
)
}

// After the user has either signed up or logged in, they will be redirected back to a `/callback` url.
// This is where you will mint a session token for
export async function GET(req: NextRequest): Promise<NextResponse> {
const url = new URL(JWT_CALLBACK_PATHNAME, DOCS_ORIGIN);
export function createCallbackUrl(
fern_token: string,
state: string | null
): URL {
const url = new URL(JWT_CALLBACK_PATHNAME, DOCS_ORIGIN)

// sends the fern_token to fern docs as a query parameter (please be sure that this is over HTTPS)
url.searchParams.set('fern_token', fern_token)

// preserve the state
const state = req.nextUrl.searchParams.get("state");
if (state) {
url.searchParams.set("state", state);
url.searchParams.set('state', state)
}

return url
}

export const dynamic = 'force-dynamic'

// After the user has either signed up or logged in, they will be redirected back to a `/callback` url.
// This is where you will mint a session token for
export async function GET(req: NextRequest): Promise<NextResponse> {
// Step 1. mint the fern_token
const fern_token = await signFernJWT({
// audience?: [] <-- set audience filters here
});
})

// send the fern_token to fern docs as a query parameter (please be sure that this is over HTTPS)
url.searchParams.set("fern_token", fern_token);
// Step 2. preserve the state
const state = req.nextUrl.searchParams.get('state')

// this will redirect to the callback url in fern docs, which will set the cookie to the "docs.yourdomain.com" domain.
// Step 3. redirect to the callback url in fern docs, which will set the cookie to the "docs.yourdomain.com" domain.
// after which, the callback url will redirect the user to the homepage, or return to the URL contained in the state query parameter.
const response = NextResponse.redirect(url);
const response = NextResponse.redirect(createCallbackUrl(fern_token, state))

// if you've minted your own cookie for your own platform, you can set it here too:
// Alternatively, if you've minted your own cookie for your own platform, you can set it here:
// response.cookies.set("access_token", mintAccessToken(req));
// response.cookies.set("refresh_token", mintRefreshToken(req));
return response;

return response
}
32 changes: 16 additions & 16 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import type { Metadata } from 'next'
import localFont from 'next/font/local'
import './globals.css'

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
src: './fonts/GeistVF.woff',
variable: '--font-geist-sans',
weight: '100 900',
})
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
src: './fonts/GeistMonoVF.woff',
variable: '--font-geist-mono',
weight: '100 900',
})

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
title: 'Create Next App',
description: 'Generated by create next app',
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode
}>) {
return (
<html lang="en">
Expand All @@ -31,5 +31,5 @@ export default function RootLayout({
{children}
</body>
</html>
);
)
}
156 changes: 62 additions & 94 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,69 @@
import Image from "next/image";
import { redirect } from 'next/navigation'
import { createCallbackUrl, signFernJWT } from './api/login/route'
import { use } from 'react'

const audiences = ['internal', 'beta']

export default function Home({
searchParams: searchParamsPromise,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const searchParams = use(searchParamsPromise)

async function handleSubmit(formData: FormData) {
'use server'
const audience = formData
.getAll('audience')
.filter((audience): audience is string =>
audiences.includes(String(audience))
)
const fern_token = await signFernJWT({
audience: audience.length > 0 ? audience : undefined,
})
const state =
typeof searchParams['state'] === 'string' ? searchParams['state'] : null
const url = createCallbackUrl(fern_token, state)
redirect(url.toString())
}

export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="https://nextjs.org/icons/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="grid min-h-screen grid-rows-[20px_1fr_20px] items-center justify-items-center gap-16 p-8 pb-20 font-[family-name:var(--font-geist-sans)] sm:p-20">
<main className="row-start-2 flex flex-col items-center gap-8 sm:items-start">
<form
className="flex min-w-[300px] flex-col gap-8"
action={handleSubmit}
>
<h1 className="text-center text-2xl font-bold">Audience Demo</h1>

<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="https://nextjs.org/icons/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
<div className="flex flex-col gap-4">
{audiences.map((audience) => (
<div key={audience} className="flex items-center justify-center">
<input
id={audience}
type="checkbox"
name="audience"
value={audience}
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
/>
<label
htmlFor={audience}
className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
{audience}
</label>
</div>
))}
</div>

<button
className="flex h-10 w-full items-center justify-center gap-2 rounded-full border border-solid border-transparent bg-foreground px-4 text-sm text-background transition-colors hover:bg-[#383838] sm:h-12 sm:px-5 sm:text-base dark:hover:bg-[#ccc]"
type="submit"
>
Read our docs
</a>
</div>
Login
</button>
</form>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
)
}
Loading

0 comments on commit 0d9a7a7

Please sign in to comment.