SDK do integracji z Centrum Logowania (CLA) i Access Hub App (AHA).
# Z GitHub (zalecane)
npm install github:AleksanderOne/cla-sdk
# lub w package.json
"cla-sdk": "github:AleksanderOne/cla-sdk"// next.config.ts
const nextConfig = {
transpilePackages: ["cla-sdk"],
};| Moduł | Wymagane | Wersja |
|---|---|---|
cla-sdk |
zod | >= 3.0.0 |
cla-sdk/next |
next | >= 14.0.0 |
cla-sdk/react |
react, react-dom | >= 18.0.0 |
cla-sdk/drizzle |
drizzle-orm | >= 0.30.0 |
// Core - klient SSO, typy, konfiguracja
import { createCLAClient, SSO_DEFAULTS } from 'cla-sdk';
// Next.js - sesja, auth, handlers, middleware
import { createAuth, createCallbackHandler, getSSOSession } from 'cla-sdk/next';
// React - komponenty UI
import { SSOProvider, LoginButton, useSSO } from 'cla-sdk/react';
// Drizzle - schema bazy danych
import { ssoConfig, createGetConfig } from 'cla-sdk/drizzle';// lib/db/schema.ts
import { createSSOConfigTable, pgSchema } from 'cla-sdk/drizzle';
// Opcja A: Public schema
export const ssoConfig = createSSOConfigTable();
// Opcja B: Custom schema
const mySchema = pgSchema('my_app');
export const ssoConfig = createSSOConfigTable(mySchema);// lib/sso-client.ts
import { createCLAClient } from 'cla-sdk';
import { createGetConfig } from 'cla-sdk/drizzle';
import { db } from '@/lib/db/drizzle';
export const cla = createCLAClient({
getConfig: createGetConfig(db),
fallback: {
apiKey: process.env.SSO_API_KEY || '',
projectSlug: process.env.SSO_CLIENT_ID || '',
centerUrl: process.env.SSO_CENTER_URL || '',
},
});// lib/auth.ts
import { createAuth } from 'cla-sdk/next';
import { cla } from './sso-client';
import { db } from '@/lib/db/drizzle';
import { eq } from 'drizzle-orm';
const { auth, requireAuth, requireAdmin } = createAuth({
client: cla,
getUser: async (userId) => {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
});
return user ?? null; // SDK wymaga null, nie undefined
},
});
export { auth, requireAuth, requireAdmin };// app/api/auth/sso-callback/route.ts
import { createCallbackHandler } from 'cla-sdk/next';
import { cla } from '@/lib/sso-client';
import { db } from '@/lib/db/drizzle';
export const { GET } = createCallbackHandler({
client: cla,
findOrCreateUser: async (ssoUser) => {
// Znajdź lub utwórz użytkownika w bazie
let user = await db.query.users.findFirst({
where: eq(users.ssoId, ssoUser.id),
});
if (!user) {
const [newUser] = await db.insert(users).values({
ssoId: ssoUser.id,
email: ssoUser.email,
name: ssoUser.name,
}).returning();
user = newUser;
}
return user;
},
});// app/api/auth/sso-logout/route.ts
import { createLogoutHandler } from 'cla-sdk/next';
import { cla } from '@/lib/sso-client';
export const { GET } = createLogoutHandler({
client: cla,
redirectUrl: '/login',
});// app/api/sso-health/route.ts
import { createHealthHandler } from 'cla-sdk/next';
import { cla } from '@/lib/sso-client';
export const { GET } = createHealthHandler({ client: cla });// app/login/page.tsx
'use client';
import { SSOProvider, LoginButton } from 'cla-sdk/react';
export default function LoginPage() {
return (
<SSOProvider healthEndpoint="/api/sso-health">
<div className="flex flex-col items-center gap-4">
<h1>Zaloguj się</h1>
<LoginButton callbackUrl="/dashboard">
Zaloguj przez SSO
</LoginButton>
</div>
</SSOProvider>
);
}// app/dashboard/page.tsx
import { requireAuth } from '@/lib/auth';
export default async function DashboardPage() {
const session = await requireAuth(); // Rzuca redirect do /login jeśli niezalogowany
return (
<div>
<h1>Witaj, {session.user.name}</h1>
</div>
);
}┌─────────────────────────────────────────────────────────────────┐
│ Twoja Aplikacja │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ React │ │ Next.js │ │ Drizzle │ │
│ │ Components │ │ Handlers │ │ Schema │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └────────────┬────┴──────────────────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ CLA Client │ │
│ │ (Core) │ │
│ └───────┬───────┘ │
│ │ │
└──────────────────────┼──────────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌───────────────┐ ┌─────────────────┐
│ Centrum │ │ Access Hub │
│ Logowania │◄─────────►│ App │
│ (CLA) │ │ (AHA) │
└───────────────┘ └─────────────────┘
OAuth2 Auth Policies
Users Kill Switch
Sessions Audit Logs
1. User → /login
2. LoginButton → redirect do CLA/authorize
3. CLA → autentykacja użytkownika
4. CLA → redirect do /api/auth/sso-callback?code=xxx
5. Callback → exchangeCodeForUser(code)
6. Callback → findOrCreateUser(ssoUser)
7. Callback → setSSOSession(session)
8. Callback → redirect do /dashboard
1. auth() → getSSOSession()
2. Sprawdź czy minęło 5 min od ostatniej weryfikacji
3. Jeśli tak → verifySessionWithCenter()
4. Jeśli sesja nieważna → clearSSOSession() + redirect /login
5. Jeśli OK → aktualizuj lastVerified
const cla = createCLAClient({
getConfig: () => Promise<SSOConfigData | null>,
fallback?: Partial<SSOConfigData> | (() => Partial<SSOConfigData>),
});
// Metody
cla.getSSOConfig() // Pobierz konfigurację
cla.exchangeCodeForUser(code, redirectUri) // OAuth2 callback
cla.verifySessionWithCenter(userId, tokenVersion) // Kill Switch
cla.getAuthPolicy() // Polityka z AHA
cla.getAllowedAuthMethods() // ['google', 'password', ...]
cla.isMfaRequired() // boolean
cla.getSessionMaxAge() // ms
cla.invalidateConfigCache() // Wyczyść cache
cla.invalidatePolicyCache()import { getSSOSession, setSSOSession, clearSSOSession } from 'cla-sdk/next';
const session = await getSSOSession();
await setSSOSession({ userId, email, name, role, expiresAt });
await clearSSOSession(cla);import { createAuth, createAuthAdminStrict } from 'cla-sdk/next';
const { auth, requireAuth, requireAdmin } = createAuth({
client: cla,
getUser: (userId) => Promise<DbUser | null>,
isUserBlocked?: (user) => boolean,
verifyInterval?: number, // default: 5 min
});
// Użycie
const session = await auth(); // null lub Session
const session = await requireAuth(); // Session lub redirect
const session = await requireAdmin(); // Session (admin) lub redirect
// Fail-closed dla admin (rzuca błąd zamiast redirect)
const { authAdminStrict } = createAuthAdminStrict(config);import {
createCallbackHandler,
createLogoutHandler,
createSetupHandler,
createHealthHandler,
} from 'cla-sdk/next';
// OAuth callback
export const { GET } = createCallbackHandler({
client: cla,
findOrCreateUser: (ssoUser) => Promise<DbUser>,
defaultRedirectUrl?: string,
onLoginSuccess?: (user, ssoUser) => Promise<void>,
isUserBlocked?: (user) => boolean,
});
// Logout
export const { GET } = createLogoutHandler({
client: cla,
redirectUrl?: string, // default: '/login'
});
// Setup (claim project)
export const { POST } = createSetupHandler({
getExistingConfig: () => Promise<SSOConfigData | null>,
saveConfig: (config) => Promise<void>,
onSetupComplete?: (config) => Promise<void>,
});
// Health check
export const { GET } = createHealthHandler({ client: cla });import { createCLAMiddleware, withCLAAuth } from 'cla-sdk/next';
// Standalone
export default createCLAMiddleware({
protectedPaths?: string[], // ['/dashboard', '/api/.*']
publicPaths?: string[], // ['/login', '/api/health']
adminPaths?: string[], // ['/admin']
loginUrl?: string, // '/login'
setupUrl?: string, // '/setup'
});
// Wrap existing middleware
export default withCLAAuth(existingMiddleware, config);import { SSOProvider, useSSO, useSSOConfig } from 'cla-sdk/react';
// Provider
<SSOProvider
healthEndpoint="/api/sso-health"
refreshInterval={30000} // default: 30s
>
{children}
</SSOProvider>
// Hooks
const { config, isLoading, error, refetch } = useSSO();
const { centerUrl, projectSlug, allowedMethods, requireMfa, isLoading } = useSSOConfig();import { LoginButton, LogoutButton, SSOSetupForm } from 'cla-sdk/react';
// Login
<LoginButton
variant="default" | "outline" | "ghost"
size="sm" | "md" | "lg"
callbackUrl="/dashboard"
className="..."
>
Zaloguj
</LoginButton>
// Logout
<LogoutButton
logoutUrl="/api/auth/sso-logout"
variant="ghost"
>
Wyloguj
</LogoutButton>
// Setup form
<SSOSetupForm
onSuccess={(config) => router.push('/dashboard')}
onError={(error) => toast.error(error)}
submitLabel="Skonfiguruj"
/>import {
ssoConfig, // Domyślna tabela (public schema)
createSSOConfigTable, // Factory dla custom schema
createDrizzleAdapter, // Pełny adapter CRUD
createGetConfig, // Helper dla createCLAClient
pgSchema, // Re-export z drizzle-orm
} from 'cla-sdk/drizzle';
// Schema
const mySchema = pgSchema('my_app');
export const ssoConfig = createSSOConfigTable(mySchema);
// Adapter
const adapter = createDrizzleAdapter({ db, table: ssoConfig });
await adapter.getConfig();
await adapter.saveConfig({ apiKey, projectSlug, centerUrl });
await adapter.deleteConfig();
// Helper dla klienta
const cla = createCLAClient({
getConfig: createGetConfig(db),
});# Wymagane
SSO_CENTER_URL=https://centrum-logowania.vercel.app
SSO_CLIENT_ID=my-project-slug
SSO_API_KEY=cl_xxxxx
# Opcjonalne
AHA_URL=https://access-hub-app.vercel.app
# Publiczne (dla React)
NEXT_PUBLIC_SSO_CENTER_URL=https://centrum-logowania.vercel.app
NEXT_PUBLIC_SSO_CLIENT_ID=my-project-slug- Sprawdź czy tabela
sso_configma rekord - Sprawdź zmienne środowiskowe fallback
- Sprawdź
/api/sso-healthendpoint
Kill Switch działa poprawnie. Sprawdź:
- Czy użytkownik istnieje w CLA
- Czy
tokenVersionsię zgadza - Czy CLA jest dostępne
- Sprawdź czy
redirect_urizgadza się z konfiguracją w CLA - Sprawdź czy
apiKeyjest poprawny - Sprawdź logi CLA
- Upewnij się, że
SSOProvideropakowuje komponenty - Sprawdź czy
healthEndpointzwraca poprawne dane - Sprawdź konsolę przeglądarki
MIT