يسجّل الموظفون الدخول إلى تطبيقك بحساب JS Staff الخاص بهم. لن يرى تطبيقك كلمة مرورهم أبدًا.
{
"user_id": 3,
"nama": "Sela",
"email": "sela@contoh.com",
"peran": "staff",
"tenant_id": 1,
"tenant_nama": "Jidanshoppu",
"tenant_slug": "jidanshoppu"
}
هذا كل شيء. يحمل peran (الدور) القيمة staff أو supervisor أو owner. تطبيقك لا يستطيع قراءة كلمة مرور أو راتب أو حضور أو سلفة أو حساب بنكي لأي شخص.
من مسؤول منصة JS Staff (يسجّلونه في /super/apps.php):
| القيمة | مثال |
|---|---|
client_id | app_a1b2c3d4e5f6a7b8 |
client_secret | sec_1a2b3c… — يُعرض مرة واحدة فقط |
يجب أن تحدّد redirect URI الخاص بك عند التسجيل. القاعدة صارمة:
https://<?php // sso-config.php — JANGAN di-commit. Isi dari env var. const SSO_BASE = 'https://staff.jidanshoppu.com'; const SSO_CLIENT_ID = 'app_xxxxxxxxxxxxxxxx'; const SSO_SECRET = 'sec_xxxxxxxxxxxxxxxx'; const SSO_REDIRECT = 'https://order.jidanshoppu.com/sso-callback.php';
<?php
// sso-mulai.php
require 'sso-config.php';
session_start();
/*
* `state` WAJIB. Acak, disimpan di sesi, dicocokkan lagi di callback.
* Tanpa ini, penyerang bisa mengirim link callback berisi kode MILIK DIA ke korban —
* korban jadi login sebagai akun penyerang, lalu mengetik data ke akun itu tanpa sadar.
*/
$_SESSION['sso_state'] = bin2hex(random_bytes(16));
$url = SSO_BASE . '/sso/authorize.php?' . http_build_query([
'client_id' => SSO_CLIENT_ID,
'redirect_uri' => SSO_REDIRECT,
'state' => $_SESSION['sso_state'],
]);
header('Location: ' . $url);
exit;
يكفي زر واحد: <a href="/sso-mulai.php">Masuk pakai JS Staff</a>
<?php
// sso-callback.php
require 'sso-config.php';
session_start();
// --- Orangnya menolak di layar persetujuan ---
if (isset($_GET['error'])) {
exit('Login dibatalkan.');
}
// --- Cocokkan state DULU, sebelum menyentuh code ---
$state = (string)($_GET['state'] ?? '');
if ($state === ''
|| !isset($_SESSION['sso_state'])
|| !hash_equals($_SESSION['sso_state'], $state)) {
// hash_equals, bukan ===: perbandingan biasa berhenti di huruf pertama yang beda,
// dan selisih waktunya bisa dipakai menebak nilainya.
exit('State tidak cocok. Ulangi dari awal.');
}
unset($_SESSION['sso_state']); // sekali pakai
$code = (string)($_GET['code'] ?? '');
if ($code === '') {
exit('Kode tidak ada.');
}
// --- Tukar kode jadi identitas (SERVER ke server) ---
$ch = curl_init(SSO_BASE . '/sso/token.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => http_build_query([
'client_id' => SSO_CLIENT_ID,
'client_secret' => SSO_SECRET,
'code' => $code,
'redirect_uri' => SSO_REDIRECT, // harus sama dengan langkah 3
]),
]);
$body = curl_exec($ch);
$kode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($kode !== 200) {
// Jangan tampilkan $body apa adanya ke pengguna — itu pesan untuk kamu, bukan dia.
error_log('SSO gagal: ' . $kode . ' ' . $body);
exit('Login gagal. Coba lagi.');
}
$data = json_decode((string)$body, true);
if (!is_array($data) || empty($data['ok'])) {
exit('Login gagal. Coba lagi.');
}
$u = $data['user'];
// --- Mulai dari sini urusan kamu ---
session_regenerate_id(true); // cegah session fixation
$_SESSION['jss_user_id'] = $u['user_id'];
$_SESSION['jss_tenant_id'] = $u['tenant_id'];
$_SESSION['nama'] = $u['nama'];
$_SESSION['peran'] = $u['peran'];
header('Location: /app.php');
exit;
client_secret عبر المتصفح يعادل مشاركة سرّ مع الجميع. Referer. user_id فريد عالميًا، وtenant_id يحدّد الشركة. كلاهما ثابت — لا يتغيّران أبدًا حتى لو تغيّرت الأسماء.
jss_user_id.ALTER TABLE staff ADD COLUMN jss_user_id INTEGER NULL; ALTER TABLE staff ADD COLUMN jss_tenant_id INTEGER NULL; CREATE UNIQUE INDEX uq_staff_jss ON staff (jss_user_id);
التدفّق في المرة الأولى:
jss_user_id = :user_id ← وُجد؟ سجّل الدخول.tenant_slug مع متجر لديك، أنشئ حساب الموظف، واحفظ jss_user_id الخاص به.tenant_slug غير معروف؟ ارفضه. لا تُنشئ متجرًا جديدًا بصمت من بيانات SSO — يمكن لأي شخص لديه حساب JS Staff أن يستحدث ذلك.state: عشوائي، في الجلسة، يُطابَق باستخدام hash_equals، يُستخدم مرة واحدة.redirect_uri مطابقًا تمامًا في الخطوتين 3 و4.| العرض | السبب غالبًا |
|---|---|
| صفحة "عنوان العودة غير مطابق" | يختلف redirect_uri عمّا تم تسجيله. تحقّق من الشرطة المائلة الزائدة، وhttp مقابل https، وسلسلة الاستعلام. |
| صفحة "تطبيق غير معروف" | client_id خاطئ، أو تم إلغاء التطبيق من قِبل مسؤول المنصة. |
401 عند /sso/token.php |
انتهت صلاحية الرمز (>60 ثانية)، أو استُخدم بالفعل، أو client_secret خاطئ، أو redirect_uri مختلف عن الخطوة 3. |
يعود بـ ?error=access_denied |
ضغط الموظف على رفض. ليس خللًا. |
يستخدم 401 عمدًا الرسالة نفسها لكل الأسباب — فتمييز "رمز خاطئ" عن "رمز مُستخدَم من قبل" يمنح المهاجم وسيلة لرسم خريطة بالرموز التي وُجدت يومًا، ولا يفيد أي شخص تكاملُه صحيح. تحقّق من الأمور الأربعة أعلاه واحدًا تلو الآخر.