sfsso-frappe

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

clientSecret server-only — tanpa prefix NUXT_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.