Skip to content

AleksanderOne/cla-sdk

Repository files navigation

CLA SDK

SDK do integracji z Centrum Logowania (CLA) i Access Hub App (AHA).

Instalacja

# 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"],
};

Peer dependencies

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

Moduły

// 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';

Szybki start

1. Schema bazy danych (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);

2. Klient CLA

// 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 || '',
  },
});

3. Auth helper

// 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 };

4. Route handlers

// 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 });

5. Strona logowania (React)

// 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>
  );
}

6. Ochrona stron (Server Component)

// 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>
  );
}

Architektura

┌─────────────────────────────────────────────────────────────────┐
│                         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

Flow logowania

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

Kill Switch (weryfikacja sesji)

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

API Reference

Core (cla-sdk)

createCLAClient(config)

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()

Next.js (cla-sdk/next)

Session

import { getSSOSession, setSSOSession, clearSSOSession } from 'cla-sdk/next';

const session = await getSSOSession();
await setSSOSession({ userId, email, name, role, expiresAt });
await clearSSOSession(cla);

Auth

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);

Handlers

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 });

Middleware

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);

React (cla-sdk/react)

Provider & Hooks

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();

Components

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"
/>

Drizzle (cla-sdk/drizzle)

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),
});

Zmienne środowiskowe

# 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

Troubleshooting

"SSO nie jest skonfigurowane"

  1. Sprawdź czy tabela sso_config ma rekord
  2. Sprawdź zmienne środowiskowe fallback
  3. Sprawdź /api/sso-health endpoint

"Sesja wygasła" po 5 minutach

Kill Switch działa poprawnie. Sprawdź:

  1. Czy użytkownik istnieje w CLA
  2. Czy tokenVersion się zgadza
  3. Czy CLA jest dostępne

"Unauthorized" w callback

  1. Sprawdź czy redirect_uri zgadza się z konfiguracją w CLA
  2. Sprawdź czy apiKey jest poprawny
  3. Sprawdź logi CLA

Komponenty React nie działają

  1. Upewnij się, że SSOProvider opakowuje komponenty
  2. Sprawdź czy healthEndpoint zwraca poprawne dane
  3. Sprawdź konsolę przeglądarki

Licencja

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors