From a73e4d0b3815456741cbfef4c2e8e52f8cbb72c7 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 24 Jun 2025 14:44:00 -0500 Subject: [PATCH] Adds oauth providers, fixes bottom page navigation with mobile support, adds apple client generator --- docs/docs-platform.yml | 44 +++ docs/scripts/generate-docs.js | 25 +- .../src/components/apple-secret-generator.tsx | 321 ++++++++++++++++++ docs/src/components/layout/root-toggle.tsx | 213 +++++++++--- .../layouts/docs-header-wrapper.tsx | 92 ++++- docs/src/components/layouts/docs.tsx | 92 ++++- docs/src/components/page-client.tsx | 41 ++- docs/src/mdx-components.tsx | 4 +- .../getting-started/auth-providers/apple.mdx | 72 ++++ .../auth-providers/bitbucket.mdx | 39 +++ .../auth-providers/discord.mdx | 45 +++ .../auth-providers/facebook.mdx | 43 +++ .../getting-started/auth-providers/github.mdx | 38 +++ .../getting-started/auth-providers/gitlab.mdx | 38 +++ .../getting-started/auth-providers/google.mdx | 41 +++ .../getting-started/auth-providers/index.mdx | 134 ++++++++ .../auth-providers/linkedin.mdx | 40 +++ .../getting-started/auth-providers/meta.json | 19 ++ .../auth-providers/microsoft.mdx | 43 +++ .../auth-providers/passkey.mdx | 55 +++ .../auth-providers/spotify.mdx | 41 +++ .../auth-providers/two-factor-auth.mdx | 59 ++++ .../auth-providers/x-twitter.mdx | 43 +++ docs/templates/meta.json | 1 + 24 files changed, 1491 insertions(+), 92 deletions(-) create mode 100644 docs/src/components/apple-secret-generator.tsx create mode 100644 docs/templates/getting-started/auth-providers/apple.mdx create mode 100644 docs/templates/getting-started/auth-providers/bitbucket.mdx create mode 100644 docs/templates/getting-started/auth-providers/discord.mdx create mode 100644 docs/templates/getting-started/auth-providers/facebook.mdx create mode 100644 docs/templates/getting-started/auth-providers/github.mdx create mode 100644 docs/templates/getting-started/auth-providers/gitlab.mdx create mode 100644 docs/templates/getting-started/auth-providers/google.mdx create mode 100644 docs/templates/getting-started/auth-providers/index.mdx create mode 100644 docs/templates/getting-started/auth-providers/linkedin.mdx create mode 100644 docs/templates/getting-started/auth-providers/meta.json create mode 100644 docs/templates/getting-started/auth-providers/microsoft.mdx create mode 100644 docs/templates/getting-started/auth-providers/passkey.mdx create mode 100644 docs/templates/getting-started/auth-providers/spotify.mdx create mode 100644 docs/templates/getting-started/auth-providers/two-factor-auth.mdx create mode 100644 docs/templates/getting-started/auth-providers/x-twitter.mdx diff --git a/docs/docs-platform.yml b/docs/docs-platform.yml index abe6da791..fb955f465 100644 --- a/docs/docs-platform.yml +++ b/docs/docs-platform.yml @@ -22,6 +22,50 @@ pages: - path: getting-started/example-pages.mdx platforms: ["js"] # Only vanilla JS + # Auth Providers - Available for all platforms since OAuth is universal + - path: getting-started/auth-providers/index.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/github.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/google.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/facebook.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/microsoft.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/spotify.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/discord.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/gitlab.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/apple.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/bitbucket.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/linkedin.mdx + platforms: ["next", "react", "js", "python"] + + - path: getting-started/auth-providers/x-twitter.mdx + platforms: ["next", "react", "js", "python"] + + # Advanced auth methods - More frontend-focused + - path: getting-started/auth-providers/passkey.mdx + platforms: ["next", "react", "js"] # No Python (frontend feature) + + - path: getting-started/auth-providers/two-factor-auth.mdx + platforms: ["next", "react", "js"] # No Python (frontend feature) + - path: getting-started/production.mdx platforms: ["next", "react", "js"] # No Python diff --git a/docs/scripts/generate-docs.js b/docs/scripts/generate-docs.js index f7076c952..671cd492e 100644 --- a/docs/scripts/generate-docs.js +++ b/docs/scripts/generate-docs.js @@ -219,9 +219,28 @@ function generateMetaFiles() { } // Regular page else { - const pagePath = `${page}.mdx`; - if (shouldIncludeFileForPlatform(platform, pagePath)) { - currentSectionPages.push(page); + // Check if this is actually a folder reference vs a page reference + // A folder reference should have a corresponding directory in templates + const folderPath = path.join(TEMPLATE_DIR, page); + const isActualFolder = fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory(); + + if (isActualFolder) { + // This is a folder reference - check if folder has content for this platform + const hasContentInFolder = platformConfig.pages.some(configPage => + configPage.path.startsWith(`${page}/`) && + configPage.platforms.includes(platform) + ); + + if (hasContentInFolder) { + currentSectionPages.push(page); + } + } else { + // This is a regular page reference + const pagePath = `${page}.mdx`; + const shouldInclude = shouldIncludeFileForPlatform(platform, pagePath); + if (shouldInclude) { + currentSectionPages.push(page); + } } } } diff --git a/docs/src/components/apple-secret-generator.tsx b/docs/src/components/apple-secret-generator.tsx new file mode 100644 index 000000000..c6406ba6d --- /dev/null +++ b/docs/src/components/apple-secret-generator.tsx @@ -0,0 +1,321 @@ +'use client'; + +import { useState } from 'react'; +import { cn } from '../lib/cn'; +import { Info } from './mdx/info'; +import { buttonVariants } from './ui/button'; + +// Simple Button component using existing buttonVariants +type ButtonProps = React.ButtonHTMLAttributes & { + variant?: 'primary' | 'outline' | 'ghost' | 'secondary', + size?: 'sm' | 'icon' | 'icon-sm', + children: React.ReactNode, +}; + +const Button = ({ variant = 'primary', size, className, children, ...props }: ButtonProps) => { + return ( + + ); +}; + +// Input component that matches the docs theme +type InputProps = React.InputHTMLAttributes & { + label?: string, + labelOptional?: string, + reveal?: boolean, + copy?: boolean, +}; + +const Input = ({ + label, + labelOptional, + reveal, + copy, + className, + ...props +}: InputProps) => { + const [isRevealed, setIsRevealed] = useState(false); + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + if (props.value && typeof props.value === 'string') { + await navigator.clipboard.writeText(props.value); + setCopied(true); + void setTimeout(() => setCopied(false), 2000); + } + }; + + return ( +
+ {label && ( +
+ + {labelOptional && ( + + {labelOptional} + + )} +
+ )} +
+ +
+ {reveal && ( + + )} + {copy && ( + + )} +
+
+
+ ); +}; + +function base64URL(value: string) { + return globalThis.btoa(value).replace(/[=]/g, '').replace(/[+]/g, '-').replace(/[\/]/g, '_'); +} + +/* +Convert a string into an ArrayBuffer +from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String +*/ +function stringToArrayBuffer(value: string) { + const buf = new ArrayBuffer(value.length); + const bufView = new Uint8Array(buf); + for (let i = 0; i < value.length; i++) { + bufView[i] = value.charCodeAt(i); + } + return buf; +} + +function arrayBufferToString(buf: ArrayBuffer) { + return String.fromCharCode.apply(null, Array.from(new Uint8Array(buf))); +} + +const generateAppleSecretKey = async ( + kid: string, + iss: string, + sub: string, + file: File +): Promise<{ kid: string, jwt: string, exp: number }> => { + if (!kid) { + const match = file.name.match(/AuthKey_([^.]+)[.].*$/i); + if (match && match[1]) { + kid = match[1]; + } + } + + if (!kid) { + throw new Error( + `No Key ID provided. The file "${file.name}" does not follow the AuthKey_XXXXXXXXXX.p8 pattern. Please provide a Key ID manually.` + ); + } + + const contents = await file.text(); + + if (!contents.match(/^\s*-+BEGIN PRIVATE KEY-+[^-]+-+END PRIVATE KEY-+\s*$/i)) { + throw new Error(`Chosen file does not appear to be a PEM encoded PKCS8 private key file.`); + } + + // remove PEM headers and spaces + const pkcs8 = stringToArrayBuffer( + globalThis.atob(contents.replace(/-+[^-]+-+/g, '').replace(/\s+/g, '')) + ); + + const privateKey = await globalThis.crypto.subtle.importKey( + 'pkcs8', + pkcs8, + { + name: 'ECDSA', + namedCurve: 'P-256', + }, + true, + ['sign'] + ); + + const iat = Math.floor(Date.now() / 1000); + const exp = iat + 180 * 24 * 60 * 60; + + const jwt = [ + base64URL(JSON.stringify({ typ: 'JWT', kid, alg: 'ES256' })), + base64URL( + JSON.stringify({ + iss, + sub, + iat, + exp, + aud: 'https://appleid.apple.com', + }) + ), + ]; + + const signature = await globalThis.crypto.subtle.sign( + { + name: 'ECDSA', + hash: 'SHA-256', + }, + privateKey, + stringToArrayBuffer(jwt.join('.')) + ); + + jwt.push(base64URL(arrayBufferToString(signature))); + + return { kid, jwt: jwt.join('.'), exp }; +}; + +const AppleSecretGenerator = () => { + const [file, setFile] = useState(null); + const [teamID, setTeamID] = useState(''); + const [serviceID, setServiceID] = useState(''); + const [keyID, setKeyID] = useState(''); + const [secretKey, setSecretKey] = useState(''); + const [expiresAt, setExpiresAt] = useState(''); + const [error, setError] = useState(''); + + return ( +
+ setTeamID(e.target.value.trim())} + /> + setServiceID(e.target.value.trim())} + /> + setKeyID(e.target.value.trim())} + /> +
+ + { + setFile(e.target.files?.[0] || null); + }} + className={cn( + 'flex h-7 w-full rounded border border-input bg-transparent px-2 py-1 text-xs', + 'file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground', + 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring', + 'disabled:cursor-not-allowed disabled:opacity-50' + )} + /> +
+ + + + {error && ( + + {error} + + )} + + {secretKey && ( + + )} +
+ ); +}; + +export default AppleSecretGenerator; diff --git a/docs/src/components/layout/root-toggle.tsx b/docs/src/components/layout/root-toggle.tsx index fa4e83d8a..9924402b1 100644 --- a/docs/src/components/layout/root-toggle.tsx +++ b/docs/src/components/layout/root-toggle.tsx @@ -1,11 +1,10 @@ 'use client'; import { usePathname } from 'fumadocs-core/framework'; import Link from 'fumadocs-core/link'; -import { ChevronsUpDown } from 'lucide-react'; -import { type ComponentProps, type ReactNode, useMemo, useState } from 'react'; +import { ChevronDown } from 'lucide-react'; +import { type ComponentProps, type ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { cn } from '../../lib/cn'; import { isActive } from '../../lib/is-active'; -import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; export type Option = { /** @@ -25,6 +24,23 @@ export type Option = { props?: ComponentProps<'a'>, } +// Platform-specific colors matching homepage +const platformColors: Record = { + 'Next.js': 'rgb(59, 130, 246)', // Blue + 'React': 'rgb(16, 185, 129)', // Green + 'JavaScript': 'rgb(245, 158, 11)', // Yellow + 'Python': 'rgb(168, 85, 247)', // Purple + 'Stack Auth Next.js': 'rgb(59, 130, 246)', + 'Stack Auth React': 'rgb(16, 185, 129)', + 'Stack Auth JavaScript': 'rgb(245, 158, 11)', + 'Stack Auth Python': 'rgb(168, 85, 247)', +}; + +function getPlatformColor(title: ReactNode): string { + const titleStr = String(title); + return platformColors[titleStr] || 'rgb(100, 116, 139)'; // fallback color +} + export function RootToggle({ options, ...props @@ -32,7 +48,9 @@ export function RootToggle({ options: Option[], } & ComponentProps<'button'>) { const [open, setOpen] = useState(false); + const [hoveredOption, setHoveredOption] = useState