Skip to content

Contoh Kode Integrasi (Code Examples)

Halaman ini menyediakan contoh implementasi kode untuk pengiriman pesan pada sisi Parent Portal serta penerimaan event listener pada sisi Child Sub-App dalam berbagai kerangka kerja (React, Vue, Vanilla JS, dan PHP/Laravel).


1. Parent Window Dispatcher (React / Next.js - Portal Utama)

Implementasi pengiriman payload autentikasi dari portal utama (superapps-fe) ke elemen <iframe>:

tsx
import React, { useEffect, useRef } from 'react';

interface SuperAppsParentIframeProps {
  subAppUrl: string; // Contoh: "https://iboss.bpbatam.go.id"
  authPayload: {
    authCode: string;
    accessToken: string;
    user: any;
  };
}

export function SuperAppsIframeContainer({ subAppUrl, authPayload }: SuperAppsParentIframeProps) {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  // Mengirimkan payload autentikasi ke Child Iframe setelah memuat
  const sendAuthToChild = () => {
    if (!iframeRef.current || !iframeRef.current.contentWindow) return;

    const targetOrigin = new URL(subAppUrl).origin;

    const messageData = {
      type: 'SUPERAPPS_AUTH_SUCCESS',
      payload: {
        authCode: authPayload.authCode,
        accessToken: authPayload.accessToken,
        user: authPayload.user,
        timestamp: Date.now(),
      },
    };

    console.log('[Parent] Mengirim kredensial SSO ke iframe:', targetOrigin);
    
    // PERHATIAN: Selalu gunakan targetOrigin spesifik, JANGAN gunakan '*'
    iframeRef.current.contentWindow.postMessage(messageData, targetOrigin);
  };

  return (
    <div className="w-full h-screen border-0">
      <iframe
        ref={iframeRef}
        src={subAppUrl}
        onLoad={sendAuthToChild}
        title="Sub-App Frame"
        className="w-full h-full border-0"
        sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads"
      />
    </div>
  );
}

2. Child Sub-App Receiver (React / Next.js)

Implementasi custom hook pada sub-aplikasi React untuk menerima dan memproses kredensial autentikasi:

tsx
import React, { useEffect, useState } from 'react';

const ALLOWED_PARENT_ORIGIN = 'https://superapps.bpbatam.go.id'; // Domain resmi Parent Portal

export function useSuperAppsIframeAuth() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [userInfo, setUserInfo] = useState<any>(null);

  useEffect(() => {
    const handleAuthMessage = async (event: MessageEvent) => {
      // 1. WAJIB: Memverifikasi Origin Pengirim Pesan
      if (event.origin !== ALLOWED_PARENT_ORIGIN) {
        console.warn('[Sub-App] Pesan dari origin tidak dikenal diabaikan:', event.origin);
        return;
      }

      // 2. Validasi Tipe Pesan Event
      if (event.data?.type !== 'SUPERAPPS_AUTH_SUCCESS') {
        return;
      }

      const { authCode, accessToken, user } = event.data.payload || {};

      console.log('[Sub-App] Menerima data autentikasi SuperApps:', user?.email);

      try {
        // 3. Mengirim Auth Code / Access Token ke Backend Sub-Aplikasi
        const response = await fetch('/api/auth/sso-login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ authCode, accessToken }),
        });

        const data = await response.json();

        if (response.ok && data.token) {
          // 4. Menyimpan Token Sesi Lokal Sub-Aplikasi
          localStorage.setItem('subapp_token', data.token);
          setUserInfo(user);
          setIsAuthenticated(true);
        }
      } catch (err) {
        console.error('[Sub-App] Gagal memproses autentikasi SSO:', err);
      }
    };

    // Pendaftaran Event Listener
    window.addEventListener('message', handleAuthMessage);

    // Pembersihan Event Listener saat Unmount
    return () => {
      window.removeEventListener('message', handleAuthMessage);
    };
  }, []);

  return { isAuthenticated, userInfo };
}

3. Child Sub-App Receiver (Vue 3 / Composition API)

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const ALLOWED_PARENT_ORIGIN = 'https://superapps.bpbatam.go.id';
const isAuthenticated = ref(false);

const handleMessage = async (event) => {
  // Verifikasi Origin & Tipe Pesan
  if (event.origin !== ALLOWED_PARENT_ORIGIN) return;
  if (event.data?.type !== 'SUPERAPPS_AUTH_SUCCESS') return;

  const { authCode, accessToken } = event.data.payload;

  // Mengirim Kredensial ke Backend Sub-Aplikasi
  const res = await fetch('/api/v1/auth/sso-callback', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code: authCode, token: accessToken })
  });

  if (res.ok) {
    const data = await res.json();
    localStorage.setItem('access_token', data.accessToken);
    isAuthenticated.value = true;
  }
};

onMounted(() => {
  window.addEventListener('message', handleMessage);
});

onUnmounted(() => {
  window.removeEventListener('message', handleMessage);
});
</script>

<template>
  <div v-if="isAuthenticated">
    <h2>Sub-Aplikasi Berhasil Terautentikasi</h2>
  </div>
  <div v-else>
    <p>Memproses Autentikasi SSO SuperApps...</p>
  </div>
</template>

4. Child Sub-App Receiver (Vanilla JavaScript / HTML Murni)

html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <title>Sub-App Portal Integrasi</title>
</head>
<body>
  <div id="status">Menunggu Autentikasi SuperApps...</div>

  <script>
    (function() {
      const ALLOWED_ORIGIN = 'https://superapps.bpbatam.go.id';

      window.addEventListener('message', function(event) {
        // 1. Verifikasi Origin
        if (event.origin !== ALLOWED_ORIGIN) return;
        if (!event.data || event.data.type !== 'SUPERAPPS_AUTH_SUCCESS') return;

        const payload = event.data.payload;
        document.getElementById('status').innerText = 'Autentikasi Berhasil: ' + payload.user.name;

        // 2. Menyimpan Token Sesi
        sessionStorage.setItem('sso_access_token', payload.accessToken);
      });
    })();
  </script>
</body>
</html>

5. Backend Verification Endpoint (PHP / Laravel)

Backend sub-aplikasi menerima authCode dari frontend iframe dan memverifikasinya secara langsung ke server Goauthentik:

php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class SsoAuthController extends Controller
{
    public function handleIframeSso(Request $request)
    {
        $request->validate([
            'authCode' => 'required|string',
        ]);

        // 1. Backend Sub-Aplikasi Melakukan HTTP POST ke Goauthentik Token Endpoint
        $response = Http::asForm()->post(config('services.authentik.token_url'), [
            'grant_type' => 'authorization_code',
            'client_id' => config('services.authentik.client_id'),
            'client_secret' => config('services.authentik.client_secret'),
            'redirect_uri' => config('services.authentik.redirect_uri'),
            'code' => $request->input('authCode'),
        ]);

        if ($response->failed()) {
            return response()->json(['error' => 'Gagal verifikasi auth code ke Goauthentik'], 401);
        }

        $tokenData = $response->json();
        
        // 2. Mengambil atau Membuat Data Pengguna Lokal
        $user = User::firstOrCreate([
            'email' => $tokenData['email'] ?? $tokenData['preferred_username']
        ], [
            'name' => $tokenData['name'] ?? 'Pengguna BP Batam',
        ]);

        // 3. Menerbitkan Token Sesi Lokal Sub-Aplikasi
        $localToken = $user->createToken('iframe-session')->plainTextToken;

        return response()->json([
            'status' => 'success',
            'token' => $localToken,
            'user' => $user
        ]);
    }
}

6. Penanganan Query Parameter (embed=true & form=...)

Implementasi pemeriksaan query parameter URL embed=true untuk menyembunyikan Navbar/Sidebar dan form=specific-form untuk pengarahan otomatis (auto-route) pengguna.

Contoh Implementasi di React / Next.js:

tsx
import React, { useEffect } from 'react';
import { useSearchParams, useNavigate } from 'next/navigation';

export function LayoutContainer({ children }: { children: React.ReactNode }) {
  const searchParams = useSearchParams();
  const navigate = useNavigate();

  // 1. Memeriksa Parameter Query
  const isEmbed = searchParams.get('embed') === 'true';
  const targetForm = searchParams.get('form');

  useEffect(() => {
    // 2. Apabila Parameter form Ditemukan, Lakukan Auto-routing
    if (targetForm) {
      console.log('[Sub-App] Pengarahan Otomatis ke Form:', targetForm);
      navigate(`/forms/${targetForm}`);
    }
  }, [targetForm, navigate]);

  return (
    <div className="min-h-screen bg-slate-50">
      {/* Menyembunyikan Navbar Apabila Mode Embed Aktif */}
      {!isEmbed && <NavbarComponent />}

      <div className="flex">
        {/* Menyembunyikan Sidebar Apabila Mode Embed Aktif */}
        {!isEmbed && <SidebarComponent />}

        {/* Konten Utama Form */}
        <main className={`flex-1 ${isEmbed ? 'p-0 m-0' : 'p-6'}`}>
          {children}
        </main>
      </div>

      {/* Menyembunyikan Footer Apabila Mode Embed Aktif */}
      {!isEmbed && <FooterComponent />}
    </div>
  );
}

Contoh Implementasi di Vue 3 / Vanilla JS:

html
<script>
  (function() {
    const urlParams = new URLSearchParams(window.location.search);
    const isEmbed = urlParams.get('embed') === 'true';
    const targetForm = urlParams.get('form');

    if (isEmbed) {
      // Menyembunyikan Elemen Navigasi Bawaan
      document.addEventListener("DOMContentLoaded", function() {
        const header = document.querySelector('header, .navbar, #sidebar');
        if (header) header.style.display = 'none';
        
        document.body.classList.add('is-embedded');
      });
    }

    if (targetForm) {
      // Pengarahan Otomatis ke Form Spesifik
      window.location.hash = '#/forms/' + targetForm;
    }
  })();
</script>

<style>
  /* Aturan CSS Saat Mode Embed Aktif */
  body.is-embedded .navbar,
  body.is-embedded .sidebar,
  body.is-embedded footer {
    display: none !important;
  }
  body.is-embedded main {
    width: 100% !important;
    margin: 0 !important;
    padding: 1rem !important;
  }
</style>

Dokumentasi Resmi Integrasi SSO & Iframe SuperApps BP Batam