pwaLUPMIS2/dist/index.php
ekke d8ddbbc910 Fix stale district + offline-safe logout; GPS UTM format; touch-cursor gating; SW v12
Session / district correctness
- public/index.php: add a /?logout=1 endpoint that destroys the PWA's own PHP
  session (session_destroy + expire PHPSESSID + clear sso_auth_token, then
  redirect to the SSO portal). Logout previously cleared only the SSO cookie,
  leaving the PHPSESSID session — and its frozen district_id — intact, which is
  why a reassigned user kept loading the old district across logout/login.
- SSO token is validated once per session, at login (unchanged first-login
  logic). A district transfer is now picked up on the next logout→login, which
  is correct precisely because logout finally tears the session down. No
  periodic SSO polling.
- main.js: the menu Logout button routes through /?logout=1 and wipes
  district-scoped local caches first. Logout is blocked while offline — a
  session can only be created online, so an offline logout would strand the
  user with no way back in (and would not actually reach the server).
- main.js: enforceDistrictConsistency() clears district-scoped caches when the
  session district changes between loads; the district boundary is cached under
  a per-district key (district_boundary_<id>) so one district's geometry can
  never be served for another.

GPS coordinate format
- New "GPS Coordinate Format" setting (Lat/Lon · UTM · Both) in the Settings
  panel; the navbar read-out renders the chosen format and repaints the current
  fix immediately on change. Self-contained WGS84→UTM converter in
  geo-utils.js, verified against an independent Redfearn-series implementation.

ol-ext touch cursor
- MapView gates the TouchCursor to genuine touch-only devices via matchMedia
  (any-pointer: fine / any-hover: hover); hybrid touchscreen laptops keep the
  normal cursor. Reactive to pointer-capability changes.

- Service worker v11 → v12 (new shell). docs/SSO_Session_Refresh_Proposal.md
  documents the implemented approach.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:58:36 +00:00

166 lines
9.5 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
// ────────────────────────────────────────────────────────────────────────────
// The PWA keeps its own PHP session (PHPSESSID on this host), independent of
// the central SSO portal. Logging out of the SSO portal clears `sso_auth_token`
// but leaves this PHPSESSID session intact, so the previous user's fields
// (incl. district_id) persist. This endpoint is what actually destroys the PWA
// session. Triggered by `/?logout=1` from the in-app menu.
if (isset($_GET['logout'])) {
$_SESSION = [];
// Expire the session cookie itself.
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();
// Clear the shared SSO cookie across all *.lupmis4luspa.org subdomains.
setcookie('sso_auth_token', '', time() - 3600, '/', '.lupmis4luspa.org');
// Bounce to the central LUSPA portal to complete SSO logout.
header('Location: https://lupmis4luspa.org/', 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;