Appearance
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>