-
Notifications
You must be signed in to change notification settings - Fork 0
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
1 parent
f127e99
commit 0d9a7a7
Showing
16 changed files
with
3,127 additions
and
1,035 deletions.
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,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 |
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,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 |
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,7 @@ | ||
{ | ||
"trailingComma": "es5", | ||
"tabWidth": 2, | ||
"semi": false, | ||
"singleQuote": true, | ||
"plugins": ["prettier-plugin-tailwindcss"] | ||
} |
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,4 @@ | ||
{ | ||
"editor.defaultFormatter": "esbenp.prettier-vscode", | ||
"npm.packageManager": "pnpm" | ||
} |
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 |
---|---|---|
@@ -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 | ||
} |
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
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 |
---|---|---|
@@ -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> | ||
); | ||
) | ||
} |
Oops, something went wrong.