Przejdź do treści

Next.js

SDK przeglądarkowe w aplikacji na App Router — leniwy klient, trasa powrotna i sprawdzanie tokenu w route handlerze.

Paczki dla Next.js nie ma i ta strona pokazuje, dlaczego nie musi być: @kleora-io/browser nie zna żadnego frameworka, a całe logowanie dzieje się w przeglądarce. Poniżej App Router; w Pages Routerze zmienia się tylko to, gdzie leżą te dwa pliki.

npm install @kleora-io/browser

1. Jeden klient, tworzony w przeglądarce

Next.js wykonuje komponent kliencki także na serwerze, więc instancja powstaje leniwie i tylko tam, gdzie istnieje przeglądarka. Sięgnięcie po location na poziomie modułu to typowy sposób, w jaki się to psuje podczas builda.

// lib/kleora.ts
import { createKleora, type KleoraClient } from '@kleora-io/browser'

let client: KleoraClient | null = null

export function getKleora(): KleoraClient {
  if (!client) {
    client = createKleora({
      issuer: process.env.NEXT_PUBLIC_KLEORA_ISSUER!,
      clientId: process.env.NEXT_PUBLIC_KLEORA_CLIENT_ID!,
      redirectUri: window.location.origin + '/callback',
      storage: 'session',
    })
  }
  return client
}

Wołaj getKleora() z efektu albo z obsługi zdarzenia — nigdy w trakcie renderowania.

2. Trasa powrotna

app/callback/page.tsx, zgodna z redirectUri powyżej i z zarejestrowanym na kliencie http://localhost:3000/callback:

'use client'

import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { getKleora } from '@/lib/kleora'

export default function Callback() {
  const router = useRouter()

  useEffect(() => {
    getKleora()
      .handleRedirectCallback()
      .then(({ appState }) => {
        const returnTo = (appState as { returnTo?: string })?.returnTo
        router.replace(returnTo ?? '/')
      })
      .catch(() => router.replace('/'))
  }, [router])

  return <p>Logujemy Cię…</p>
}

3. Hook dla reszty aplikacji

'use client'

import { useEffect, useState } from 'react'
import { getKleora } from '@/lib/kleora'
import type { User } from '@kleora-io/browser'

export function useUser() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const kleora = getKleora()
    const unsubscribe = kleora.onAuthChange((state) => setUser(state.user))

    kleora
      .checkSession()
      .then(() => setUser(kleora.getUser()))
      .finally(() => setLoading(false))

    return unsubscribe
  }, [])

  return { user, loading }
}

Wysłanie kogoś do logowania to wtedy jedno wywołanie — z obsługi zdarzenia albo z efektu na stronie, która tego wymaga:

await getKleora().loginWithRedirect({ returnTo: window.location.pathname })

4. Sprawdź token w route handlerze

Twoje route handlery to API jak każde inne i sprawdzają token dostępu offline, względem zestawu kluczy publikowanego przez issuer. @kleora-io/node deklaruje Node 20+, więc uruchom trasę na runtime nodejs, nie na edge.

// app/api/invoices/route.ts
import { verifyAccessToken, TokenError } from '@kleora-io/node'

export const runtime = 'nodejs'

export async function GET(request: Request) {
  const header = request.headers.get('authorization') ?? ''
  if (!header.startsWith('Bearer ')) {
    return Response.json({ detail: 'Unauthorized' }, { status: 401 })
  }

  try {
    const claims = await verifyAccessToken(header.slice(7), {
      issuer: process.env.KLEORA_ISSUER!,
      audience: process.env.KLEORA_AUDIENCE!,
    })
    if (!claims.permissions.includes('invoices:read')) {
      return Response.json({ detail: 'Forbidden' }, { status: 403 })
    }
    return Response.json({ tenant: claims.tenant })
  } catch (error) {
    if (error instanceof TokenError && error.code === 'issuer_unavailable') {
      return Response.json({ detail: 'Issuer unavailable' }, { status: 503 })
    }
    return Response.json({ detail: 'Unauthorized' }, { status: 401 })
  }
}

verifyAccessToken przyjmuje goły token, czyli dokładnie to, co daje obiekt Headers z Request. Drugie wejście, authenticate(), oczekuje obiektu z właściwością headers.authorization — to kształt z Express, nie z Fetch API.

audience to api_audience Twojego środowiska, jeśli je ustawiłeś, a w przeciwnym razie identyfikator klienta. Zestaw kluczy pobierany jest raz i trzymany tak długo, jak mówi jego Cache-Control; nieosiągalny issuer to issuer_unavailable, czyli 503 od Ciebie zamiast 401, który powiedziałby każdemu wołającemu, że jego token jest zły.

Wysłanie tokenu to połowa po stronie klienta:

const token = await getKleora().getAccessToken()
const response = await fetch('/api/invoices', {
  headers: { Authorization: `Bearer ${token}` },
})