Nuxt 3+ (Nitro server + client)
Nuxt spesial: punya Nitro server bawaan. Bisa pakai sso-frappe/server di Nitro server routes dan sso-frappe/browser di client — tanpa backend terpisah.
Arsitektur
Nuxt client (Vue)
│ link ke /api/auth/frappe/start
▼
Halaman authorize Frappe
│ redirect balik dengan code + state
▼
Nuxt Nitro server route
│ exchangeCode() + getUserProfile()
▼
Nuxt session (cookie)Env vars
NUXT_PUBLIC_FRAPPE_SSO_BASE_URL=https://erp.example.com
NUXT_PUBLIC_FRAPPE_SSO_CLIENT_ID=<oauth-client-id>
FRAPPE_SSO_CLIENT_SECRET=<secret>
FRAPPE_SSO_REDIRECT_URI=https://app.example.com/api/auth/frappe/callback
clientSecretserver-only — tanpa prefixNUXT_PUBLIC_.
Opsi A — Flow dikelola backend (rekomendasi)
Server Nitro mengelola seluruh flow. Client cukup link ke /api/auth/frappe/start.
Server route: start
// server/api/auth/frappe/start.ts
import { createFrappeSSO } from 'sso-frappe/server';
export default defineEventHandler(async (event) => {
const sso = createFrappeSSO({
baseUrl: process.env.NUXT_PUBLIC_FRAPPE_SSO_BASE_URL!,
clientId: process.env.NUXT_PUBLIC_FRAPPE_SSO_CLIENT_ID!,
clientSecret: process.env.FRAPPE_SSO_CLIENT_SECRET!,
redirectUri: process.env.FRAPPE_SSO_REDIRECT_URI!,
});
const { url, state, codeVerifier } = await sso.createAuthorizationUrl();
setCookie(event, 'frappe_oauth_state', state, {
httpOnly: true,
sameSite: 'lax',
maxAge: 600,
});
setCookie(event, 'frappe_oauth_verifier', codeVerifier!, {
httpOnly: true,
sameSite: 'lax',
maxAge: 600,
});
return sendRedirect(event, url);
});Server route: callback
// server/api/auth/frappe/callback.ts
import { createFrappeSSO, validateState } from 'sso-frappe/server';
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const code = query.code as string;
const state = query.state as string;
const storedState = getCookie(event, 'frappe_oauth_state') ?? '';
const codeVerifier = getCookie(event, 'frappe_oauth_verifier') ?? '';
validateState(storedState, state);
const sso = createFrappeSSO({ /* ... */ });
const token = await sso.exchangeCode({ code, codeVerifier });
const profile = await sso.getUserProfile(token);
// Match/create user (lihat /docs/id/database-schema)
const user = await upsertUserFromFrappe(profile);
setCookie(event, 'session', createSessionToken(user), {
httpOnly: true,
sameSite: 'lax',
maxAge: 86400,
});
deleteCookie(event, 'frappe_oauth_state');
deleteCookie(event, 'frappe_oauth_verifier');
return sendRedirect(event, '/dashboard');
});Client: tombol login
<!-- components/FrappeLoginButton.vue -->
<script setup lang="ts">
const props = withDefaults(defineProps<{ label?: string }>(), {
label: 'Login with Frappe',
});
</script>
<template>
<NuxtLink to="/api/auth/frappe/start" class="rounded-lg bg-slate-900 px-4 py-2 text-white">
{{ label }}
</NuxtLink>
</template>Route middleware (auth guard)
// middleware/auth.ts
export default defineNuxtRouteMiddleware(async () => {
const { session, fetchSession } = useAuth();
if (!session.value) await fetchSession();
if (!session.value) return navigateTo('/login');
});Opsi B — PKCE dikelola browser
Gunakan sso-frappe/browser di client (sama seperti panduan Vue SPA). Server Nitro hanya menangani POST callback.
Pilih opsi mana?
| Opsi A (backend) | Opsi B (browser PKCE) | |
|---|---|---|
| State storage | Server cookie (httpOnly) | sessionStorage |
| Kompleksitas client | Cuma link | Import + composable |
| Cocok untuk | Production, SSR | Pure SPA, static hosting |
Rekomendasi: Opsi A untuk kebanyakan app Nuxt. Server sudah ada — gunakan.