Spaces:
Sleeping
Sleeping
change: integration docs
Browse files- README.md +23 -67
- resources/js/pages/embed.tsx +36 -24
README.md
CHANGED
|
@@ -51,38 +51,21 @@ ### 1. Quick Start
|
|
| 51 |
|
| 52 |
### 2. Authentication
|
| 53 |
|
| 54 |
-
Widget
|
| 55 |
-
|
| 56 |
-
#### 2.1 API Key (Direkomendasikan)
|
| 57 |
-
|
| 58 |
-
Gunakan parameter `api_key`. Widget akan secara otomatis menukar API Key dengan bearer token melalui endpoint `/auth/login/api-key` saat pertama kali dimuat.
|
| 59 |
|
| 60 |
```
|
| 61 |
/embed?api_key=sk-xxxxxxxxxxxxxxxx
|
| 62 |
```
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
> **Keamanan:** API Key sebaiknya tidak di-hardcode langsung di HTML publik. Untuk lingkungan produksi, generate token di sisi server lalu teruskan lewat metode **Direct Token** (lihat 2.2).
|
| 67 |
-
|
| 68 |
-
#### 2.2 Direct Token
|
| 69 |
-
|
| 70 |
-
Jika server Anda sudah memiliki bearer token yang valid, teruskan langsung melalui parameter `token`:
|
| 71 |
-
|
| 72 |
-
```
|
| 73 |
-
/embed?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
| 74 |
-
```
|
| 75 |
-
|
| 76 |
-
Token akan langsung disimpan ke `sessionStorage` dan digunakan untuk semua request selanjutnya.
|
| 77 |
|
| 78 |
#### Token Resolution Priority
|
| 79 |
|
| 80 |
Widget menyelesaikan autentikasi dengan urutan berikut:
|
| 81 |
|
| 82 |
-
1. Cek
|
| 83 |
-
2. Cek parameter `?
|
| 84 |
-
3.
|
| 85 |
-
4. Jika tidak ada ketiganya → widget menampilkan pesan error autentikasi.
|
| 86 |
|
| 87 |
---
|
| 88 |
|
|
@@ -94,8 +77,7 @@ #### 3.1 Authentication
|
|
| 94 |
|
| 95 |
| Parameter | Tipe | Keterangan |
|
| 96 |
|-----------|----------|------------|
|
| 97 |
-
| `api_key` | `string` | API Key dari Admin Panel. Widget otomatis login dan menyimpan token. |
|
| 98 |
-
| `token` | `string` | Bearer token langsung. Digunakan jika server sudah generate token sendiri. |
|
| 99 |
|
| 100 |
#### 3.2 Behavior
|
| 101 |
|
|
@@ -129,7 +111,7 @@ ### 4. Theming & Customization
|
|
| 129 |
| `text_primary` | `#0a0a0a` | Warna teks utama. |
|
| 130 |
| `text_secondary` | `#737373` | Warna teks sekunder / placeholder. |
|
| 131 |
| `border` | `#739af1` | Warna border composer dan bubble. |
|
| 132 |
-
| `
|
| 133 |
| `radius` | `20px` | Border radius bubble chat dan composer. |
|
| 134 |
|
| 135 |
**Contoh URL dengan tema gelap:**
|
|
@@ -143,7 +125,7 @@ ### 4. Theming & Customization
|
|
| 143 |
&primary=4f8ef7
|
| 144 |
&primary_gradient=1e3a6e
|
| 145 |
&border=2a3f6f
|
| 146 |
-
&
|
| 147 |
&radius=16px
|
| 148 |
```
|
| 149 |
|
|
@@ -153,7 +135,7 @@ ### 5. postMessage Events
|
|
| 153 |
|
| 154 |
Widget berkomunikasi dengan halaman induk melalui `window.postMessage`. Dengarkan event ini di halaman yang meng-embed iframe untuk mendapat notifikasi dari widget.
|
| 155 |
|
| 156 |
-
####
|
| 157 |
|
| 158 |
| Event type | Kapan dikirim |
|
| 159 |
|-----------------------------|---------------|
|
|
@@ -177,22 +159,22 @@ #### 6.1 Events yang Dikirim Widget (Outbound)
|
|
| 177 |
|
| 178 |
### 6. Session Persistence
|
| 179 |
|
| 180 |
-
Widget menyimpan data sesi di
|
| 181 |
|
| 182 |
-
|
|
| 183 |
-
|------------------------------|-----|
|
| 184 |
-
| `sevima_raghub_embed_token` | Bearer token aktif. |
|
| 185 |
-
| `sevima_raghub_embed_user` | Objek user JSON (`id`, `name`, `email`, `role`). |
|
| 186 |
|
| 187 |
-
|
| 188 |
|
| 189 |
-
Untuk memaksa
|
| 190 |
|
| 191 |
---
|
| 192 |
|
| 193 |
### 7. Full Integration Examples
|
| 194 |
|
| 195 |
-
####
|
| 196 |
|
| 197 |
```html
|
| 198 |
<!DOCTYPE html>
|
|
@@ -225,7 +207,7 @@ #### 8.1 Embed Sederhana (API Key)
|
|
| 225 |
</html>
|
| 226 |
```
|
| 227 |
|
| 228 |
-
####
|
| 229 |
|
| 230 |
```html
|
| 231 |
<style>
|
|
@@ -290,7 +272,7 @@ #### 8.2 Floating Chat Button
|
|
| 290 |
</script>
|
| 291 |
```
|
| 292 |
|
| 293 |
-
####
|
| 294 |
|
| 295 |
```html
|
| 296 |
<iframe
|
|
@@ -305,39 +287,15 @@ #### 8.3 Pra-pilih Mata Kuliah & Langsung ke Sesi Tertentu
|
|
| 305 |
></iframe>
|
| 306 |
```
|
| 307 |
|
| 308 |
-
#### 8.4 Token dari Server (Lebih Aman untuk Produksi)
|
| 309 |
-
|
| 310 |
-
Alur yang direkomendasikan untuk produksi: server Anda meminta token ke RAG Hub API menggunakan API Key, lalu meneruskan token tersebut ke frontend.
|
| 311 |
-
|
| 312 |
-
```javascript
|
| 313 |
-
// server-side (Node.js / PHP / Python, dll.)
|
| 314 |
-
const response = await fetch('https://your-raghub-domain.com/api/auth/login/api-key', {
|
| 315 |
-
method: 'POST',
|
| 316 |
-
headers: { 'Content-Type': 'application/json' },
|
| 317 |
-
body: JSON.stringify({ api_key: process.env.RAG_API_KEY }),
|
| 318 |
-
});
|
| 319 |
-
const { token } = await response.json();
|
| 320 |
-
|
| 321 |
-
// Render ke template HTML
|
| 322 |
-
const iframeSrc = `https://your-raghub-domain.com/embed?token=${token}&user_name=${userName}`;
|
| 323 |
-
```
|
| 324 |
-
|
| 325 |
-
```html
|
| 326 |
-
<!-- HTML yang dirender server -->
|
| 327 |
-
<iframe src="{{ iframeSrc }}" width="420" height="680" style="border:none;"></iframe>
|
| 328 |
-
```
|
| 329 |
-
|
| 330 |
---
|
| 331 |
|
| 332 |
### 8. Troubleshooting
|
| 333 |
|
| 334 |
#### Widget menampilkan "Token iframe tidak tersedia"
|
| 335 |
|
| 336 |
-
Pastikan
|
| 337 |
-
- `?api_key=` — API Key valid dari Admin Panel.
|
| 338 |
-
- `?token=` — Bearer token yang belum expired.
|
| 339 |
|
| 340 |
-
Jika
|
| 341 |
|
| 342 |
#### Mata kuliah tidak muncul di dropdown
|
| 343 |
|
|
@@ -349,15 +307,13 @@ #### Widget tidak merespons tombol ✕ di halaman induk
|
|
| 349 |
|
| 350 |
#### Token expired di tengah sesi
|
| 351 |
|
| 352 |
-
Widget tidak melakukan refresh token otomatis. Jika token expired, reload iframe dengan token baru:
|
| 353 |
|
| 354 |
```javascript
|
| 355 |
const iframe = document.getElementById('raghub-iframe');
|
| 356 |
-
iframe.src =
|
| 357 |
```
|
| 358 |
|
| 359 |
-
Reload iframe akan menghapus `sessionStorage`-nya dan memulai sesi baru.
|
| 360 |
-
|
| 361 |
#### Warna tidak berubah setelah mengubah query string
|
| 362 |
|
| 363 |
Pastikan nilai warna dalam format hex yang valid: 3 atau 6 digit, dengan atau tanpa `#`. Karakter selain `0-9` dan `a-f` akan diabaikan dan diganti nilai default.
|
|
|
|
| 51 |
|
| 52 |
### 2. Authentication
|
| 53 |
|
| 54 |
+
Widget menggunakan API Key untuk autentikasi. Saat pertama kali dimuat, widget menukar API Key dengan bearer token melalui endpoint `/auth/login/api-key`, lalu menyimpan token tersebut ke cookie browser selama **7 hari**.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
```
|
| 57 |
/embed?api_key=sk-xxxxxxxxxxxxxxxx
|
| 58 |
```
|
| 59 |
|
| 60 |
+
Selama cookie masih valid, widget tidak akan login ulang meski halaman di-refresh. Cookie otomatis dihapus setelah 7 hari atau jika browser cookie dibersihkan.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
#### Token Resolution Priority
|
| 63 |
|
| 64 |
Widget menyelesaikan autentikasi dengan urutan berikut:
|
| 65 |
|
| 66 |
+
1. Cek cookie — jika token sudah tersimpan dan belum expired, langsung digunakan.
|
| 67 |
+
2. Cek parameter `?api_key` di URL, tukar dengan token via API, simpan ke cookie.
|
| 68 |
+
3. Jika tidak ada keduanya → widget menampilkan pesan error autentikasi.
|
|
|
|
| 69 |
|
| 70 |
---
|
| 71 |
|
|
|
|
| 77 |
|
| 78 |
| Parameter | Tipe | Keterangan |
|
| 79 |
|-----------|----------|------------|
|
| 80 |
+
| `api_key` | `string` | API Key dari Admin Panel. Widget otomatis login dan menyimpan token ke cookie. |
|
|
|
|
| 81 |
|
| 82 |
#### 3.2 Behavior
|
| 83 |
|
|
|
|
| 111 |
| `text_primary` | `#0a0a0a` | Warna teks utama. |
|
| 112 |
| `text_secondary` | `#737373` | Warna teks sekunder / placeholder. |
|
| 113 |
| `border` | `#739af1` | Warna border composer dan bubble. |
|
| 114 |
+
| `danger` | `#ef4444` | Warna aksi destruktif (tombol hapus sesi). |
|
| 115 |
| `radius` | `20px` | Border radius bubble chat dan composer. |
|
| 116 |
|
| 117 |
**Contoh URL dengan tema gelap:**
|
|
|
|
| 125 |
&primary=4f8ef7
|
| 126 |
&primary_gradient=1e3a6e
|
| 127 |
&border=2a3f6f
|
| 128 |
+
&danger=ff6b6b
|
| 129 |
&radius=16px
|
| 130 |
```
|
| 131 |
|
|
|
|
| 135 |
|
| 136 |
Widget berkomunikasi dengan halaman induk melalui `window.postMessage`. Dengarkan event ini di halaman yang meng-embed iframe untuk mendapat notifikasi dari widget.
|
| 137 |
|
| 138 |
+
#### 5.1 Events yang Dikirim Widget (Outbound)
|
| 139 |
|
| 140 |
| Event type | Kapan dikirim |
|
| 141 |
|-----------------------------|---------------|
|
|
|
|
| 159 |
|
| 160 |
### 6. Session Persistence
|
| 161 |
|
| 162 |
+
Widget menyimpan data sesi di **cookie browser** dengan masa berlaku 7 hari. Cookie diset pada domain RAG Hub (domain iframe), sehingga tidak dapat diakses oleh halaman induk.
|
| 163 |
|
| 164 |
+
| Cookie name | Isi | TTL |
|
| 165 |
+
|------------------------------|-----|-----|
|
| 166 |
+
| `sevima_raghub_embed_token` | Bearer token aktif. | 7 hari |
|
| 167 |
+
| `sevima_raghub_embed_user` | Objek user JSON (`id`, `name`, `email`, `role`). | 7 hari |
|
| 168 |
|
| 169 |
+
Selama cookie masih valid, widget tidak akan memanggil ulang `/auth/login/api-key` meski halaman di-refresh atau tab ditutup lalu dibuka kembali.
|
| 170 |
|
| 171 |
+
Untuk memaksa login ulang (reset sesi), reload iframe dengan mengganti `src` — cookie lama akan diabaikan saat API Key baru ditukar.
|
| 172 |
|
| 173 |
---
|
| 174 |
|
| 175 |
### 7. Full Integration Examples
|
| 176 |
|
| 177 |
+
#### 7.1 Embed Sederhana
|
| 178 |
|
| 179 |
```html
|
| 180 |
<!DOCTYPE html>
|
|
|
|
| 207 |
</html>
|
| 208 |
```
|
| 209 |
|
| 210 |
+
#### 7.2 Floating Chat Button
|
| 211 |
|
| 212 |
```html
|
| 213 |
<style>
|
|
|
|
| 272 |
</script>
|
| 273 |
```
|
| 274 |
|
| 275 |
+
#### 7.3 Pra-pilih Mata Kuliah & Langsung ke Sesi Tertentu
|
| 276 |
|
| 277 |
```html
|
| 278 |
<iframe
|
|
|
|
| 287 |
></iframe>
|
| 288 |
```
|
| 289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
---
|
| 291 |
|
| 292 |
### 8. Troubleshooting
|
| 293 |
|
| 294 |
#### Widget menampilkan "Token iframe tidak tersedia"
|
| 295 |
|
| 296 |
+
Pastikan parameter `?api_key=` ada di URL `src` iframe dan API Key tersebut masih aktif serta belum direvoke di Admin Panel.
|
|
|
|
|
|
|
| 297 |
|
| 298 |
+
Jika cookie lama ada tapi sudah expired, hapus cookie `sevima_raghub_embed_token` dari browser lalu reload.
|
| 299 |
|
| 300 |
#### Mata kuliah tidak muncul di dropdown
|
| 301 |
|
|
|
|
| 307 |
|
| 308 |
#### Token expired di tengah sesi
|
| 309 |
|
| 310 |
+
Widget tidak melakukan refresh token otomatis. Jika token expired, cukup reload iframe — widget akan menukar `api_key` yang sama dengan token baru dan menyimpannya ke cookie:
|
| 311 |
|
| 312 |
```javascript
|
| 313 |
const iframe = document.getElementById('raghub-iframe');
|
| 314 |
+
iframe.src = iframe.src; // trigger reload
|
| 315 |
```
|
| 316 |
|
|
|
|
|
|
|
| 317 |
#### Warna tidak berubah setelah mengubah query string
|
| 318 |
|
| 319 |
Pastikan nilai warna dalam format hex yang valid: 3 atau 6 digit, dengan atau tanpa `#`. Karakter selain `0-9` dan `a-f` akan diabaikan dan diganti nilai default.
|
resources/js/pages/embed.tsx
CHANGED
|
@@ -37,7 +37,7 @@ type EmbedConfig = {
|
|
| 37 |
backgroundProfile: string;
|
| 38 |
border: string;
|
| 39 |
courseId?: string | null;
|
| 40 |
-
|
| 41 |
primary: string;
|
| 42 |
primaryGradient: string;
|
| 43 |
radius: string;
|
|
@@ -53,7 +53,7 @@ const COLOR_DEFAULTS = {
|
|
| 53 |
background: '#ffffff',
|
| 54 |
backgroundProfile: '#ffffff',
|
| 55 |
border: '#739af1',
|
| 56 |
-
|
| 57 |
primary: '#2764eb',
|
| 58 |
primaryGradient: '#ccdcff',
|
| 59 |
textPrimary: '#0a0a0a',
|
|
@@ -94,7 +94,7 @@ function parseEmbedConfig(params: URLSearchParams): EmbedConfig {
|
|
| 94 |
backgroundProfile: parseColor(params.get('background_profile'), COLOR_DEFAULTS.backgroundProfile),
|
| 95 |
border: parseColor(params.get('border'), COLOR_DEFAULTS.border),
|
| 96 |
courseId: params.get('course_id') ?? params.get('courseId') ?? undefined,
|
| 97 |
-
|
| 98 |
primary: parseColor(params.get('primary'), COLOR_DEFAULTS.primary),
|
| 99 |
primaryGradient: parseColor(params.get('primary_gradient'), COLOR_DEFAULTS.primaryGradient),
|
| 100 |
radius: parseRadius(params.get('radius')),
|
|
@@ -106,17 +106,39 @@ function parseEmbedConfig(params: URLSearchParams): EmbedConfig {
|
|
| 106 |
};
|
| 107 |
}
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
function getStoredEmbedUser(): AuthUser | null {
|
| 118 |
try {
|
| 119 |
-
const raw =
|
| 120 |
return raw ? (JSON.parse(raw) as AuthUser) : null;
|
| 121 |
} catch {
|
| 122 |
return null;
|
|
@@ -124,13 +146,11 @@ function getStoredEmbedUser(): AuthUser | null {
|
|
| 124 |
}
|
| 125 |
|
| 126 |
function storeEmbedSession(token: string, user?: AuthUser): void {
|
| 127 |
-
|
| 128 |
-
sessionStorage.setItem(EMBED_TOKEN_KEY, token);
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
} catch {}
|
| 134 |
}
|
| 135 |
|
| 136 |
async function resolveEmbedToken(
|
|
@@ -142,14 +162,6 @@ async function resolveEmbedToken(
|
|
| 142 |
return { token: stored, user: getStoredEmbedUser() ?? undefined };
|
| 143 |
}
|
| 144 |
|
| 145 |
-
const tokenParam = params.get('token');
|
| 146 |
-
|
| 147 |
-
if (tokenParam) {
|
| 148 |
-
storeEmbedSession(tokenParam);
|
| 149 |
-
|
| 150 |
-
return { token: tokenParam };
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
const apiKey = params.get('api_key');
|
| 154 |
|
| 155 |
if (apiKey) {
|
|
@@ -498,7 +510,7 @@ export default function Embed() {
|
|
| 498 |
const rootStyle = {
|
| 499 |
'--embed-bg': config.background,
|
| 500 |
'--embed-border': config.border,
|
| 501 |
-
'--embed-danger': config.
|
| 502 |
'--embed-muted': config.textSecondary,
|
| 503 |
'--embed-primary': config.primary,
|
| 504 |
'--embed-primary-soft': config.primaryGradient,
|
|
|
|
| 37 |
backgroundProfile: string;
|
| 38 |
border: string;
|
| 39 |
courseId?: string | null;
|
| 40 |
+
danger: string;
|
| 41 |
primary: string;
|
| 42 |
primaryGradient: string;
|
| 43 |
radius: string;
|
|
|
|
| 53 |
background: '#ffffff',
|
| 54 |
backgroundProfile: '#ffffff',
|
| 55 |
border: '#739af1',
|
| 56 |
+
danger: '#ef4444',
|
| 57 |
primary: '#2764eb',
|
| 58 |
primaryGradient: '#ccdcff',
|
| 59 |
textPrimary: '#0a0a0a',
|
|
|
|
| 94 |
backgroundProfile: parseColor(params.get('background_profile'), COLOR_DEFAULTS.backgroundProfile),
|
| 95 |
border: parseColor(params.get('border'), COLOR_DEFAULTS.border),
|
| 96 |
courseId: params.get('course_id') ?? params.get('courseId') ?? undefined,
|
| 97 |
+
danger: parseColor(params.get('danger'), COLOR_DEFAULTS.danger),
|
| 98 |
primary: parseColor(params.get('primary'), COLOR_DEFAULTS.primary),
|
| 99 |
primaryGradient: parseColor(params.get('primary_gradient'), COLOR_DEFAULTS.primaryGradient),
|
| 100 |
radius: parseRadius(params.get('radius')),
|
|
|
|
| 106 |
};
|
| 107 |
}
|
| 108 |
|
| 109 |
+
const COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
|
| 110 |
+
|
| 111 |
+
function setEmbedCookie(name: string, value: string): void {
|
| 112 |
+
const secure = window.location.protocol === 'https:';
|
| 113 |
+
const parts = [
|
| 114 |
+
`${name}=${encodeURIComponent(value)}`,
|
| 115 |
+
'path=/',
|
| 116 |
+
`max-age=${COOKIE_MAX_AGE}`,
|
| 117 |
+
'SameSite=None',
|
| 118 |
+
];
|
| 119 |
+
|
| 120 |
+
if (secure) {
|
| 121 |
+
parts.push('Secure');
|
| 122 |
}
|
| 123 |
+
|
| 124 |
+
document.cookie = parts.join('; ');
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
function getEmbedCookie(name: string): string | null {
|
| 128 |
+
const match = document.cookie.match(
|
| 129 |
+
new RegExp(`(?:^|;\\s*)${name}=([^;]*)`),
|
| 130 |
+
);
|
| 131 |
+
|
| 132 |
+
return match ? decodeURIComponent(match[1]) : null;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
function getStoredEmbedToken(): string | null {
|
| 136 |
+
return getEmbedCookie(EMBED_TOKEN_KEY);
|
| 137 |
}
|
| 138 |
|
| 139 |
function getStoredEmbedUser(): AuthUser | null {
|
| 140 |
try {
|
| 141 |
+
const raw = getEmbedCookie(EMBED_USER_KEY);
|
| 142 |
return raw ? (JSON.parse(raw) as AuthUser) : null;
|
| 143 |
} catch {
|
| 144 |
return null;
|
|
|
|
| 146 |
}
|
| 147 |
|
| 148 |
function storeEmbedSession(token: string, user?: AuthUser): void {
|
| 149 |
+
setEmbedCookie(EMBED_TOKEN_KEY, token);
|
|
|
|
| 150 |
|
| 151 |
+
if (user) {
|
| 152 |
+
setEmbedCookie(EMBED_USER_KEY, JSON.stringify(user));
|
| 153 |
+
}
|
|
|
|
| 154 |
}
|
| 155 |
|
| 156 |
async function resolveEmbedToken(
|
|
|
|
| 162 |
return { token: stored, user: getStoredEmbedUser() ?? undefined };
|
| 163 |
}
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
const apiKey = params.get('api_key');
|
| 166 |
|
| 167 |
if (apiKey) {
|
|
|
|
| 510 |
const rootStyle = {
|
| 511 |
'--embed-bg': config.background,
|
| 512 |
'--embed-border': config.border,
|
| 513 |
+
'--embed-danger': config.danger,
|
| 514 |
'--embed-muted': config.textSecondary,
|
| 515 |
'--embed-primary': config.primary,
|
| 516 |
'--embed-primary-soft': config.primaryGradient,
|