sfsso-frappe

Vue SPA + backend proxy

Arsitektur sama dengan React — sso-frappe/browser di client, sso-frappe/server di backend.

Env frontend

VITE_FRAPPE_SSO_BASE_URL=https://erp.example.com
VITE_FRAPPE_SSO_CLIENT_ID=<oauth-client-id>
VITE_FRAPPE_SSO_REDIRECT_URI=http://localhost:5173/auth/callback

1. Composable

// src/composables/useFrappeSSO.ts
import { createFrappeSSOBrowser } from 'sso-frappe/browser';

const sso = createFrappeSSOBrowser({
  baseUrl: import.meta.env.VITE_FRAPPE_SSO_BASE_URL,
  clientId: import.meta.env.VITE_FRAPPE_SSO_CLIENT_ID,
  redirectUri: import.meta.env.VITE_FRAPPE_SSO_REDIRECT_URI,
  allowInsecureHttp: true, // dev lokal
});

export function useFrappeSSO() {
  return {
    startLogin: () => sso.startLogin(),
    handleCallback: () => sso.handleCallback(),
    clear: () => sso.clear(),
  };
}

2. Tombol login

<!-- src/components/FrappeLoginButton.vue -->
<script setup lang="ts">
import { useFrappeSSO } from '@/composables/useFrappeSSO';

const { startLogin } = useFrappeSSO();
</script>

<template>
  <button @click="startLogin" class="rounded-lg bg-slate-900 px-4 py-2 text-white">
    <slot>Login with Frappe</slot>
  </button>
</template>

Penggunaan:

<FrappeLoginButton />
<FrappeLoginButton>Masuk dengan akun perusahaan</FrappeLoginButton>

3. Halaman callback

<!-- src/pages/FrappeCallback.vue -->
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useFrappeSSO } from '@/composables/useFrappeSSO';

const router = useRouter();
const { handleCallback, clear } = useFrappeSSO();
const error = ref<string | null>(null);

onMounted(async () => {
  try {
    const { code, codeVerifier } = await handleCallback();

    const res = await fetch('/api/auth/frappe/callback', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ code, codeVerifier }),
    });

    if (!res.ok) throw new Error('SSO callback failed');

    clear();
    router.push('/dashboard');
  } catch (err) {
    error.value = err instanceof Error ? err.message : 'Unknown error';
  }
});
</script>

<template>
  <p v-if="error">Login gagal: {{ error }}</p>
  <p v-else>Menyelesaikan login...</p>
</template>

4. Backend

Identik dengan contoh Express di panduan React. Backend-nya framework-agnostic — Vue hanya mengirim { code, codeVerifier } via fetch.

Pinia store (opsional)

// src/stores/auth.ts
import { defineStore } from 'pinia';
import { useFrappeSSO } from '@/composables/useFrappeSSO';

export const useAuthStore = defineStore('auth', () => {
  const { startLogin, handleCallback, clear } = useFrappeSSO();

  async function loginWithFrappe() {
    await startLogin();
  }

  async function completeFrappeCallback() {
    const { code, codeVerifier } = await handleCallback();
    const res = await fetch('/api/auth/frappe/callback', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ code, codeVerifier }),
    });
    if (!res.ok) throw new Error('SSO callback failed');
    clear();
  }

  return { loginWithFrappe, completeFrappeCallback };
});