The /?logout=1 endpoint destroyed only the PWA's own PHP session and then redirected to the bare landing page — which has no session and immediately blocks access, so the user was never actually logged out of SSO. - public/index.php: after session_destroy(), redirect to https://lupmis4luspa.org/user-logout (the portal's full SSO logout) instead of the landing page. Crucially, no longer clear sso_auth_token here — /user-logout needs that cookie to identify which SSO session to terminate (and it clears the cookie itself). The production access-guard bounce to the landing page is unchanged. - main.js: drop the now-redundant best-effort client call to /sso/logout; the server redirect chain (/?logout=1 → /user-logout) owns the SSO logout. Offline guard and district-cache wipe unchanged. - sw.js: update the v12 changelog note (still v12; not yet deployed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
173 lines
9.9 KiB
PHP
173 lines
9.9 KiB
PHP
<?php
|
|
/**
|
|
* LUPMIS2 PWA — Authenticated entry point
|
|
*
|
|
* This file replaces a plain index.html as the directory index in production.
|
|
* It:
|
|
* 1. Picks up the LUSPA SSO cookie (sso_auth_token) set by the central
|
|
* login at https://lupmis4luspa.org/sso/.
|
|
* 2. Validates the token server-side against the SSO endpoint.
|
|
* 3. Populates a PHP session with the authenticated user's profile
|
|
* (user_id, district_id, region_id, full_name, ua_id, …).
|
|
* 4. Reads the built index.html that Vite produces and injects the
|
|
* session payload as a JavaScript global `window.LUPMIS_SESSION` —
|
|
* the PWA reads this on startup (see src/remotedb.js) to scope every
|
|
* API call to the logged-in user's district.
|
|
*
|
|
* In local development (Vite serves index.html directly without PHP) the
|
|
* global is absent and the PWA falls back to a hard-coded district for
|
|
* testing. See remotedb.js getApiCredentials().
|
|
*
|
|
* Adapted from auth code provided by the LUSPA authentication team
|
|
* (FromKwesi / 20260527 / index.php).
|
|
*/
|
|
session_start();
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Logout — end the PWA's OWN session, then hand off to the central SSO logout
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// The PWA keeps its own PHP session (PHPSESSID on this host), independent of
|
|
// the central SSO portal, so the previous user's fields (incl. district_id)
|
|
// persist until this session is destroyed. We do that here, then redirect to
|
|
// the portal's `/user-logout`, which performs the FULL SSO logout (invalidates
|
|
// the token server-side and clears the sso_auth_token cookie itself).
|
|
//
|
|
// Important: we deliberately do NOT clear `sso_auth_token` here. `/user-logout`
|
|
// needs that cookie to identify which SSO session to terminate — expiring it
|
|
// first would leave the SSO session alive on the server. Redirecting to the
|
|
// bare landing page (the old behaviour) only dropped the user on a page that
|
|
// then blocked them; it never logged them out of SSO.
|
|
//
|
|
// Triggered by `/?logout=1` from the in-app menu.
|
|
if (isset($_GET['logout'])) {
|
|
$_SESSION = [];
|
|
// Expire the PWA's own session cookie (PHPSESSID).
|
|
if (ini_get('session.use_cookies')) {
|
|
$cp = session_get_cookie_params();
|
|
setcookie(session_name(), '', time() - 42000,
|
|
$cp['path'], $cp['domain'], $cp['secure'], $cp['httponly']);
|
|
}
|
|
session_destroy();
|
|
// Hand off to the central SSO logout (full logout: invalidates the token
|
|
// and clears sso_auth_token). Keep the cookie intact so it can do so.
|
|
header('Location: https://lupmis4luspa.org/user-logout', true, 302);
|
|
exit;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// SSO authentication — validate once per session, at login
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// A user's district changes only when they are transferred — a rare event — so
|
|
// the validated fields (district_id, region_id, name, …) are valid for the
|
|
// whole session. We therefore validate the SSO token only when there is no
|
|
// session yet (i.e. at login) and do NOT re-poll SSO on every load.
|
|
//
|
|
// A transfer is picked up the next time the user logs out and back in: the
|
|
// Logout endpoint above destroys the PHP session, so the subsequent login has
|
|
// no session and re-validates, fetching the new district_id. This is correct
|
|
// precisely because logout now tears the session down (previously it only
|
|
// cleared the SSO cookie, which left the stale district pinned).
|
|
if (!isset($_SESSION['user_id']) && isset($_COOKIE['sso_auth_token'])) {
|
|
$plainToken = $_COOKIE['sso_auth_token'];
|
|
$validate_url = 'https://lupmis4luspa.org/sso/validate?token=' . urlencode($plainToken);
|
|
|
|
$curl = curl_init();
|
|
curl_setopt_array($curl, [
|
|
CURLOPT_URL => $validate_url,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_ENCODING => "",
|
|
CURLOPT_MAXREDIRS => 10,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
|
CURLOPT_CUSTOMREQUEST => "GET",
|
|
CURLOPT_HTTPHEADER => [ "Content-Type: application/xml" ],
|
|
]);
|
|
$response = curl_exec($curl);
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|
curl_close($curl);
|
|
|
|
if ($httpCode === 200) {
|
|
$data = json_decode($response, true);
|
|
if (
|
|
is_array($data)
|
|
&& isset($data['valid']) && $data['valid'] === true
|
|
&& isset($data['logged_in_user']) && is_array($data['logged_in_user'])
|
|
) {
|
|
// Copy all returned user fields into the session
|
|
foreach ($data['logged_in_user'] as $key => $value) {
|
|
$_SESSION[$key] = $value;
|
|
}
|
|
}
|
|
} else {
|
|
// Token rejected by the SSO server — clear the stale cookie so the
|
|
// browser stops sending it. Domain `.lupmis4luspa.org` covers all
|
|
// subdomains (so SSO logout works from the PWA too).
|
|
setcookie('sso_auth_token', '', time() - 3600, '/', '.lupmis4luspa.org');
|
|
}
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Production access guard
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// On the public production host, calling this file without a valid SSO session
|
|
// is not allowed — bounce the visitor to the central LUSPA login portal. On
|
|
// local development (any host that is not *.lupmis4luspa.org, e.g. localhost,
|
|
// 127.0.0.1, a developer's .local hostname) the guard is bypassed so the PHP
|
|
// entry point can still be exercised directly during testing.
|
|
$host = $_SERVER['HTTP_HOST'] ?? '';
|
|
$isProduction = (bool) preg_match('/(^|\.)lupmis4luspa\.org$/i', $host);
|
|
if ($isProduction && !isset($_SESSION['user_id'])) {
|
|
header('Location: https://lupmis4luspa.org/', true, 302);
|
|
exit;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Build the payload exposed to the PWA as window.LUPMIS_SESSION
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
$payload = [];
|
|
if (isset($_SESSION['user_id'])) {
|
|
$payload = [
|
|
'user_id' => $_SESSION['user_id'] ?? null,
|
|
'ua_id' => $_SESSION['ua_id'] ?? null,
|
|
'username' => $_SESSION['username'] ?? null,
|
|
'title' => $_SESSION['title'] ?? null,
|
|
'full_name' => $_SESSION['full_name'] ?? null,
|
|
'email' => $_SESSION['email'] ?? null,
|
|
'user_type' => $_SESSION['user_type'] ?? null,
|
|
'phone' => $_SESSION['phone'] ?? null,
|
|
'ua_position' => $_SESSION['ua_position'] ?? null,
|
|
'region_id' => $_SESSION['region_id'] ?? null,
|
|
'district_id' => $_SESSION['district_id'] ?? null,
|
|
];
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Read the built index.html and inject the session as a JS global
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
$indexPath = __DIR__ . '/index.html';
|
|
$html = is_readable($indexPath)
|
|
? file_get_contents($indexPath)
|
|
: '<!DOCTYPE html><html><body><h1>LUPMIS2 PWA</h1><p>index.html is missing from this deployment.</p></body></html>';
|
|
|
|
// Encode safely for inline <script> — the JSON flags below escape
|
|
// characters that could break the HTML parser (<, >, &, ', ").
|
|
$sessionJson = json_encode(
|
|
$payload,
|
|
JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
|
|
);
|
|
$inject = "<script>window.LUPMIS_SESSION = {$sessionJson};</script>";
|
|
|
|
// Insert right after the opening <head> tag
|
|
$html = preg_replace('/<head\b[^>]*>/i', '$0' . "\n " . $inject, $html, 1);
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Serve
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
// Don't let intermediaries cache an authenticated response — the next visit
|
|
// might be a different user. Asset hashes still let static files be cached.
|
|
header('Cache-Control: no-store, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
|
|
echo $html;
|