A comprehensive starter project for working with the Agility CMS Management SDK featuring authentication components, server-side route protection, and modern React components.
- 🔐 OAuth 2.0 Authentication - Secure authentication with Agility CMS
- ⚛️ React Components - Pre-built authentication UI components
- 🎨 Customizable Themes - Dark, light, auto, and custom themes
- 🔗 Management SDK Integration - Direct integration with @agility/management-sdk
- 📱 Responsive Design - Works on all screen sizes
- 🛡️ Server-side Route Protection - Next.js middleware for secure routes
- 🍪 HTTP-only Cookies - Secure token storage
- 🛡️ TypeScript Support - Full TypeScript definitions included
- Node.js (v18 or higher recommended)
- pnpm (v9 or higher recommended)
- TurboRepo (installed as a dev dependency)
pnpm iturbo devThis will start:
- Web app at
http://localhost:3000 - Documentation at
http://localhost:3001
- Navigate to
http://localhost:3000 - Click "Authenticate with Agility"
- Complete the OAuth flow in the popup
- You'll be redirected to the protected dashboard
agilitycms-management-sdk-starter/
├── apps/
│ ├── web/ # Main Next.js application
│ └── docs/ # Documentation site
├── packages/
│ ├── auth-tools/ # Authentication components & utilities
│ ├── ui/ # Shared UI components
│ ├── eslint-config/ # Shared ESLint configuration
│ └── typescript-config/ # Shared TypeScript configuration
├── middleware.ts # Server-side route protection
└── turbo.json # Turborepo configuration
Wrap your app with the AuthProvider to enable authentication context:
// app/layout.tsx
import { AuthProvider } from '@agility/auth-tools/components';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}A clean, full-featured login panel for unauthenticated users:
import { LoginPanel } from '@agility/auth-tools/components';
import { useRouter } from 'next/navigation';
function LoginPage() {
const router = useRouter();
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 flex items-center justify-center">
<div className="w-full max-w-md mx-auto">
<LoginPanel
config={{
title: "Agility CMS",
subtitle: "Sign in to your account",
buttonText: "Authenticate with Agility",
theme: "dark",
onSignIn: () => {
router.push("/protected");
}
}}
/>
</div>
</div>
);
}A floating top bar for authenticated users:
import { TopBar } from '@agility/auth-tools/components';
import { useRouter } from 'next/navigation';
function ProtectedPage() {
const router = useRouter();
return (
<div className="min-h-screen bg-white dark:bg-gray-900">
<TopBar
config={{
theme: 'auto',
showUserInfo: true,
showWebsiteSelector: true,
showLocaleSelector: true,
showSignOutButton: true,
onSignOut: () => {
router.push('/');
},
onWebsiteSelect: (website) => {
console.log('Selected website:', website.websiteName);
},
onLocaleSelect: (locale) => {
console.log('Selected locale:', locale.localeCode);
}
}}
/>
<main className="pt-20 p-8">
{/* Your protected content */}
</main>
</div>
);
}A simple authentication button for custom layouts:
import { AuthButton } from '@agility/auth-tools/components';
function CustomComponent() {
return (
<div className="flex items-center space-x-4">
<h1>My App</h1>
<AuthButton
config={{
buttonText: "Sign In",
theme: "light",
onSignIn: () => console.log("User signed in"),
onSignOut: () => console.log("User signed out")
}}
/>
</div>
);
}The project includes Next.js middleware for server-side route protection using HTTP-only cookies:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookieTokenStorage } from '@agility/auth-tools/auth';
const PROTECTED_ROUTES = ['/protected'];
const AUTH_ROUTES = ['/'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const authenticated = isAuthenticated(request);
// Redirect unauthenticated users from protected routes
if (PROTECTED_ROUTES.some(route => pathname.startsWith(route)) && !authenticated) {
return NextResponse.redirect(new URL('/', request.url));
}
// Redirect authenticated users from auth routes
if (AUTH_ROUTES.includes(pathname) && authenticated) {
return NextResponse.redirect(new URL('/protected', request.url));
}
return NextResponse.next();
}
function isAuthenticated(request: NextRequest): boolean {
const tokens = cookieTokenStorage.getTokensFromRequest(request);
return tokens?.accessToken ? isValidToken(tokens.accessToken) : false;
}const config = {
theme: "dark", // "light" | "dark" | "auto" | "custom"
// Custom theme colors (when theme: "custom")
customColors: {
primary: "#6366f1",
secondary: "#8b5cf6",
background: "#1f2937",
surface: "#374151",
text: "#f9fafb",
textSecondary: "#d1d5db",
border: "#4b5563",
error: "#ef4444",
success: "#10b981"
}
};interface AgilityAuthConfig {
// Basic settings
title?: string;
subtitle?: string;
buttonText?: string;
signOutText?: string;
// OAuth settings
redirectUri?: string;
scope?: string;
region?: 'usa' | 'eu';
// UI settings
theme?: 'light' | 'dark' | 'auto' | 'custom';
mode?: 'panel' | 'button-only';
className?: string;
// Feature toggles
showUserInfo?: boolean;
showWebsiteSelector?: boolean;
showLocaleSelector?: boolean;
showSignOutButton?: boolean;
showTokenInfo?: boolean;
// Custom colors (when theme: "custom")
customColors?: {
primary?: string;
secondary?: string;
background?: string;
surface?: string;
text?: string;
textSecondary?: string;
border?: string;
error?: string;
success?: string;
};
// Callbacks
onSignIn?: (user: ServerUser) => void;
onSignOut?: () => void;
onWebsiteSelect?: (website: WebsiteAccess) => void;
onLocaleSelect?: (locale: LocaleInfo) => void;
onError?: (error: string) => void;
}The main authentication hook:
import { useAgilityAuth } from '@agility/auth-tools/components';
function MyComponent() {
const {
// Authentication state
isAuthenticated,
isLoading,
error,
user,
websiteAccess,
selectedWebsite,
locales,
selectedLocale,
tokenInfo,
// Methods
authenticate,
signOut,
selectWebsite,
selectLocale,
clearError,
// Computed values
isAuthReady
} = useAgilityAuth({
redirectUri: "http://localhost:3000/auth-callback.html",
scope: "openid profile email offline_access",
region: "usa",
autoCheckAuth: true
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!isAuthenticated) {
return <button onClick={authenticate}>Sign In</button>;
}
return (
<div>
<h1>Welcome, {user?.emailAddress}!</h1>
<p>Websites: {websiteAccess.length}</p>
<button onClick={signOut}>Sign Out</button>
</div>
);
}Hook for managing website and locale selection:
import { useWebsiteSelection } from '@agility/auth-tools/components';
function WebsiteSelector() {
const {
websiteAccess,
selectedWebsite,
locales,
selectedLocale,
selectWebsite,
selectLocale,
isLoading,
error
} = useWebsiteSelection();
return (
<div>
<select
value={selectedWebsite?.websiteGuid || ''}
onChange={(e) => {
const website = websiteAccess.find(w => w.websiteGuid === e.target.value);
if (website) selectWebsite(website);
}}
>
<option value="">Select Website</option>
{websiteAccess.map(website => (
<option key={website.websiteGuid} value={website.websiteGuid}>
{website.websiteName}
</option>
))}
</select>
</div>
);
}import { configureSdkAdapter } from '@agility/auth-tools/adapters';
import { useAgilityAuth } from '@agility/auth-tools/components';
import * as ManagementSDK from '@agility/management-sdk';
// Configure the adapter (do this once in your app)
configureSdkAdapter(ManagementSDK);
function ContentManager() {
const { isAuthenticated, selectedWebsite } = useAgilityAuth();
const [pages, setPages] = useState([]);
useEffect(() => {
if (isAuthenticated && selectedWebsite) {
fetchPages();
}
}, [isAuthenticated, selectedWebsite]);
const fetchPages = async () => {
try {
const client = new ManagementSDK.ApiClient();
const pagesResponse = await client.pageManagementMethods.getPages({
websiteGuid: selectedWebsite.websiteGuid
});
setPages(pagesResponse.pages || []);
} catch (error) {
console.error('Failed to fetch pages:', error);
}
};
return (
<div>
<h2>Pages ({pages.length})</h2>
{pages.map(page => (
<div key={page.pageID}>
{page.name} - {page.path}
</div>
))}
</div>
);
}The project includes API routes for server-side token management:
// app/api/auth/callback/route.ts
export async function POST(request: NextRequest) {
const { code, redirectUri, region } = await request.json();
// Exchange code for tokens with Agility OAuth
const tokenResponse = await fetch('https://mgmt.aglty.io/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri,
}),
});
const tokens = await tokenResponse.json();
const response = NextResponse.json({ success: true });
// Store tokens in HTTP-only cookies
cookieTokenStorage.setCookiesOnResponse({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : undefined,
}, response);
return response;
}// app/api/auth/status/route.ts
export async function GET(request: NextRequest) {
const tokens = cookieTokenStorage.getTokensFromRequest(request);
if (!tokens?.accessToken || (tokens.expiresAt && tokens.expiresAt <= Math.floor(Date.now() / 1000))) {
return NextResponse.json({ authenticated: false });
}
return NextResponse.json({
authenticated: true,
expiresAt: tokens.expiresAt
});
}# Install dependencies
pnpm install
# Start all development servers
pnpm dev
# Build all packages
pnpm build
# Run linting
pnpm lint
# Type checking
pnpm type-check# Build all apps and packages
pnpm build
# Start production server
pnpm startMIT