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